数据库作业核心功能
一.完整业务流程
- 用户发起抢票 → 触发 POST /api/v1/tickets/book
- 座位状态校验 → Redis 检查 seat:{seat_id}:status 是否为 0
- 分布式锁抢占 → Redis SET lock💺{seat_id} 1 EX 5 NX 原子操作
- 数据库乐观锁更新 → MySQL UPDATE seats SET status=1, version=version+1 WHERE id=? AND version=?
- 创建订单 → MySQL 写入 orders 表,状态为 PENDING
- 加入延时队列 → Redis ZADD orders:timeout {order_id}:
- 超时回收 → 后台协程检测到超时,更新订单为 CANCELLED ,释放座位
- 支付成功 → 更新订单为 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)
关键设计 :
- 事务一致性 :MySQL 操作在一个事务中完成, db.commit() 成功后才更新 Redis
- 乐观锁保护 :订单更新条件包含 status == "PENDING" ,防止重复处理
- 双写一致性 :先更新数据库,再更新 Redis,确保最终一致性
- 队列清理 :无论成功与否,都执行 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 生命周期管理器,在应用启动时创建后台协程任务,实现常驻运行。
四.原则回顾
- 异步优先 :全链路使用 async/await ,最大化 IO 并发能力
- Redis 挡刀 :读请求优先访问 Redis,仅写操作落库
- 乐观锁 + 分布式锁 :双重锁机制保证高并发下的数据一致性
- 逻辑外键 :避免物理外键在高并发下的死锁问题
- Decimal 精确计算 :金额字段使用 DECIMAL(10,2) ,避免浮点精度丢失
- 资源自愈 :超时订单自动回收,避免资源长期占用
- 限流保护 :每秒每用户 2 次请求,防止接口被刷爆
浙公网安备 33010602011771号