数据库作业核心功能

一.完整业务流程

  1. 用户发起抢票 → 触发 POST /api/v1/tickets/book
  2. 座位状态校验 → Redis 检查 seat:{seat_id}:status 是否为 0
  3. 分布式锁抢占 → Redis SET lock💺{seat_id} 1 EX 5 NX 原子操作
  4. 数据库乐观锁更新 → MySQL UPDATE seats SET status=1, version=version+1 WHERE id=? AND version=?
  5. 创建订单 → MySQL 写入 orders 表,状态为 PENDING
  6. 加入延时队列 → Redis ZADD orders:timeout {order_id}:
  7. 超时回收 → 后台协程检测到超时,更新订单为 CANCELLED ,释放座位
  8. 支付成功 → 更新订单为 PAID ,座位状态变为 2

二.高并发防御架构:

**第一道防线:基于 Redis 的计数器限流
对应文件 : app/limiter.py

核心逻辑 (第8-23行):
async def check_rate_limit(user_id: 
int, redis, max_requests=2, 
window_seconds=1):
    limiter_key = f"rate_limit:user:
    {user_id}"
    current_count = await redis.incr
    (limiter_key)  # 原子递增
    if current_count == 1:
        await redis.expire
        (limiter_key, 
        window_seconds)  # 设置过期时
        间
    if current_count > max_requests:
        raise HTTPException
        (status_code=429, detail="请
        求过于频繁")

原理 :使用 Redis INCR 原子操作实现计数器,每个用户每秒最多允许 2 次请求。通过 expire 自动重置窗口,避免内存泄漏。

挂载位置 : app/api/tickets.py 第24行

@router.post("/book", dependencies=
[Depends(rate_limiter)])

**第二道防线:基于 Redis 内存状态的缓存拦截
对应文件 : app/api/tickets.py

核心逻辑 (第34-39行):

current_status = await redis.get
(seat_status_key)  # seat:{seat_id}
:status
if current_status is None:
    raise HTTPException
    (status_code=404, detail="座位不
    存在")
if current_status != "0":
    raise HTTPException
    (status_code=400, detail="该座位
    已被锁定或已售出")

原理 :在进入数据库操作前,先检查 Redis 中缓存的座位状态。如果状态不为 0 (可选),直接拒绝请求,避免无效的数据库查询。

**第三道防线:基于 Redis 的排他分布式锁
对应文件 : app/api/tickets.py

核心逻辑 (第41-44行):

lock_key = f"lock💺{payload.
seat_id}"
is_locked = await redis.set
(lock_key, "1", ex=5, nx=True)
if not is_locked:
    raise HTTPException
    (status_code=409, detail="抢票失
    败,座位已被抢占")

原理 :使用 Redis SET key value EX seconds NX 命令实现原子锁:

  • EX 5 :锁过期时间 5 秒,防止死锁
  • NX :仅在 key 不存在时设置,保证同一座位只有一个请求能获得锁
    双重检查机制 (第47-50行):获取锁后再次验证座位状态,防止在获取锁期间状态已被修改。

**第四道防线:MySQL 数据库层面的乐观锁机制
对应文件 : app/api/tickets.py

核心逻辑 (第78-92行):

old_version = seat.version
update_result = await db.execute(
    update(Seat)
    .where(Seat.id == payload.
    seat_id, Seat.version == 
    old_version)
    .values(status=1, 
    version=old_version + 1)
)
if update_result.rowcount == 0:
    raise HTTPException
    (status_code=409, detail="抢票失
    败,座位已被抢占")

原理 :通过 version 字段实现乐观锁:

  • 查询时读取当前 version 值
  • 更新时条件包含 version == old_version
  • 若 rowcount == 0 ,说明版本已变更,乐观锁失败
    数据库表结构支持 : app/models.py 第72行

version: Mapped[int] = mapped_column
(nullable=False, default=0)

将四层防御换一个角度,从终端输出的信息来看:
大白话讲述一遍整体的流程:
Redis加数据库协同作战:
两次把关
第一次:收到前端的请求时,Redis自动监测,如果状态不对直接报400错误,快速拦截,状态对了之后,如果并发状态下多条请求通过那么开始看锁,没抢到锁的直接409拒绝,抢到锁的将Redis中的状态置为1,余票-1,发送给MySQL,这就是在Redis上的第一次把关。
第二次:收到Redis发送的信息之后,查找ORM版本是否正确,数据是否正确,然后执行乐观锁,如果发现对不上(一般是有人擅自修改数据库,没有通过前端),直接409拒绝,同时回滚Redis状态,如果能对上,那就将数据异步写入表中
最后再加一层保险,如果代码出现了bug,那就DB Rollback+Redis回滚,全部回滚
全部没意外的话,那就200 OK,下单成功。

三.超时自动回退机制
核心代码剖析
对应文件 : app/core/scheduler.py
3.1 延时队列查询
代码位置 :第25-28行

current_timestamp = int(time.time())
timeout_orders = await redis.zrangebyscore(
TIMEOUT_QUEUE_KEY, 0, current_timestamp
)

原理 :使用 Redis ZSet 的 ZRANGEBYSCORE 命令,查询 score(过期时间戳)小于等于当前时间的订单,实现延时队列的精确筛选。

3.2 原子状态回退
代码位置 :第64-91行
async with AsyncSessionLocal() as db:
# 乐观锁更新订单状态
update_result = await db.execute(
update(Order)
.where(Order.id == order_id, Order.status == "PENDING")
.values(status="CANCELLED")
)

回退座位状态

await db.execute(
update(Seat)
.where(Seat.id == seat_id, Seat.status == 1)
.values(status=0)
)

恢复余票

await db.execute(
update(Train)
.where(Train.id == train_id)
.values(remaining_seats=Train.remaining_seats + 1)
)

await db.commit()

更新 Redis 缓存

await redis.set(f"seat:{seat_id}:status", "0")
await redis.incrby(f"train:{train_id}:remaining_seats", 1)
await redis.zrem(TIMEOUT_QUEUE_KEY, order_id_str)

关键设计 :

  1. 事务一致性 :MySQL 操作在一个事务中完成, db.commit() 成功后才更新 Redis
  2. 乐观锁保护 :订单更新条件包含 status == "PENDING" ,防止重复处理
  3. 双写一致性 :先更新数据库,再更新 Redis,确保最终一致性
  4. 队列清理 :无论成功与否,都执行 ZREM 移除订单,避免重复处理

4.3 后台任务启动
对应文件 : app/main.py 第15行
@asynccontextmanager
async def lifespan(app: FastAPI):
await warm_up_cache()
asyncio.create_task(process_timeout_orders()) # 启动后台协程
yield

原理 :利用 FastAPI 的 lifespan 生命周期管理器,在应用启动时创建后台协程任务,实现常驻运行。

四.原则回顾

  1. 异步优先 :全链路使用 async/await ,最大化 IO 并发能力
  2. Redis 挡刀 :读请求优先访问 Redis,仅写操作落库
  3. 乐观锁 + 分布式锁 :双重锁机制保证高并发下的数据一致性
  4. 逻辑外键 :避免物理外键在高并发下的死锁问题
  5. Decimal 精确计算 :金额字段使用 DECIMAL(10,2) ,避免浮点精度丢失
  6. 资源自愈 :超时订单自动回收,避免资源长期占用
  7. 限流保护 :每秒每用户 2 次请求,防止接口被刷爆
posted @ 2026-07-15 20:28  暗神酱  阅读(11)  评论(0)    收藏  举报