12306 春节高峰
→ 返回 高并发
春节购票是另一种极端高并发:定点放票、查询风暴、库存强一致(不能超卖)、下单路径极短。与 微信步数日榜(可最终一致、可合并写)形成对照,是理解「什么时候必须上数据库事务 / 原子扣库存」的标杆场景。
与微信步数日榜的差异
| 维度 | 微信步数日榜 | 12306 春节高峰 |
|---|
| 核心数据 | 步数、排名 | 余票库存、订单 |
| 一致性 | 秒级~分钟级可接受 | 库存必须强一致,严禁超卖 |
| 写语义 | 合并、取 max | 扣减库存,成功或失败二选一 |
| 读语义 | 好友榜实时 | 余票查询极高 QPS,可短暂缓存但下单前需校验 |
| 峰值形态 | 0 点上报洪峰 | 放票时刻查询 + 抢票双高峰 |
| 用户体验 | 同步失败可稍后重试 | 排队、验证码、候票大厅 |
| 数据层角色 | 滞后落库即可 | 订单与库存是核心写路径 |
步数榜: 近似正确 > 绝对正确(体验优先)
抢票: 绝对正确 > 查询快 1 秒(库存优先)
业务特征
| 特征 | 说明 |
|---|
| 流量集中 | 春运数日,特定车次起售时间点(如 8:00、10:00) |
| 读远大于写 | 大量「查余票、查车次」;成功下单占比低 |
| 热点车次 | 少数线路/车次成为超级热 key |
| 长尾失败 | 绝大多数请求买不到票,但不能拖垮系统 |
| 合规与公平 | 防刷、限流、排队,避免机器抢票挤占 |
总体架构
用户打开 12306
│
▼
┌──────────────┐ 静态资源、页面壳
│ CDN │
└──────┬───────┘
│
▼
┌──────────────┐ 全局限流、WAF、验证码、登录态
│ 接入 / 网关 │
└──────┬───────┘
│
├──────────────────────┐
▼ ▼
┌──────────────┐ ┌──────────────┐
│ 候票 / 排队 │ │ 查询服务 │ 车次、时刻表、余票展示
│ (流量塑形) │ │ 多级缓存 │ 可短 TTL,标注「仅供参考」
└──────┬───────┘ └──────┬───────┘
│ 获得购票资格/令牌 │
▼ │
┌──────────────┐ │
│ 下单服务 │◄─────────────┘ 提交前再次校验余票
│ 原子扣库存 │
└──────┬───────┘
│
├─► Redis / DB 库存(强一致扣减)
├─► MQ 异步(出票、通知、支付超时取消)
└─► 订单库分库分表持久化
原则:查询可缓冲;扣库存与创建订单必须在受控短路径内完成,且幂等、可审计。
接入层:排队与流量塑形
公开报道与行业实践常见 「候票大厅 / 排队」 形态,本质是 数据层外的缓冲:
| 机制 | 作用 |
|---|
| 虚拟排队 | 前端显示等待,后端按令牌速率放行购票请求 |
| 令牌桶 | 每秒只允许 N 个请求进入下单链路 |
| 验证码 | 抬高机器抢票成本 |
| 登录 + 实名 | 绑定用户维度限流 |
放票瞬间 100 万在线
→ 仅 5 万/分钟 取得「购票令牌」
→ 其余用户继续排队或提示繁忙
→ 保护下单与数据库
详见 缓冲与流量塑形、API 网关。
读路径:查询风暴
可缓存的数据
| 数据 | 变更频率 | 缓存 |
|---|
| 车站、线路、时刻表 | 低 | CDN + Redis 长 TTL |
| 车次列表(某日某站) | 中 | Redis,分钟级 |
| 余票数量展示 | 高 | 极短 TTL(秒级)或静态「有/无」 |
# 示意:余票展示缓存(非扣库存依据)
GET ticket:remain:G1234:20240210:二等座
TTL 3~10 秒
关键:页面显示的「还剩 5 张」不能作为下单扣减依据;下单必须走 库存服务实时扣减。
查询削峰
- 读写分离:查询打从库或只读副本
- 结果聚合页:减少 N 次细粒度查询
- 降级:高峰返回「系统繁忙,请稍后」,保核心下单
写路径:库存与订单(核心)
严禁超卖
| 手段 | 说明 |
|---|
| 数据库行锁 | SELECT ... FOR UPDATE 扣减 remain |
| 乐观锁 | UPDATE ... SET remain=remain-1, version=version+1 WHERE version=? AND remain>0 |
| Redis 预扣 + 异步落库 | Lua 原子 DECR,成功后再写订单(需对账与回滚策略) |
| 唯一约束 | (train, date, seat_no) 唯一,插入失败即无票 |
-- Redis 预扣库存示意(需与 DB 对账)
local remain = tonumber(redis.call('GET', KEYS[1]) or '0')
if remain <= 0 then return 0 end
redis.call('DECR', KEYS[1])
return 1
下单短路径
持购票令牌 + 登录态
→ 校验乘车人、车次、席别
→ 库存服务原子扣减(失败立即返回「无票」)
→ 创建订单(待支付)
→ 返回订单号(同步路径控制在百毫秒~数百毫秒级,视架构而定)
→ 支付、出票可走异步
| 步骤 | 是否可异步 |
|---|
| 扣库存 | ❌ 必须同步成功/失败明确 |
| 写订单主表 | 通常同步(或 TCC 事务消息) |
| 发短信、排队候补 | ✅ MQ |
幂等
- 客户端
requestId / 订单幂等键
- 重复提交返回同一订单号,不二次扣库存
热点车次与分片
| 问题 | 方案 |
|---|
| 单车次库存热 key | Redis 分 key、库存分桶(按车厢/席别)、或直写 DB 热点行 + 排队 |
| 订单表膨胀 | 按 出发日期 / user_id 分库分表 |
| 查询热点 | 本地缓存 + 副本 |
见 数据层设计、ShardingSphere。
时间与放票窗口
与 时间窗口设计 类似,但是 日历点 + 起售钟点:
车次 G1234 2024-02-10 出发
起售时间:2024-01-15 08:00:00
08:00:00 前:可查询计划,不可买
08:00:00 后:库存服务开放扣减
需处理:
- 时钟同步(NTP)
- 提前排队用户在 8:00 准点放行令牌
- 08:00 后缓存余票立即失效或主动刷新
异步与 MQ
| 用途 | 说明 |
|---|
| 支付超时关单 | 延迟消息 30 分钟未支付 → 回滚库存 |
| 候补购票 | 队列排队,有票释放时通知 |
| 日志与审计 | 全链路订单事件入 Kafka |
| 削峰填谷 | 非核心通知、统计 |
下单扣库存本身不宜完全异步(用户需明确成败);MQ 用于后续状态机。
见 消息队列。
缓存策略小结(12306 场景)
| 层级 | 内容 |
|---|
| CDN | 页面、JS、车站字典 |
| Redis | 车次查询、短 TTL 余票展示、会话、排队令牌 |
| 本地缓存 | 极热车站/线路字典 |
| DB | 库存真相源、订单、支付 |
与 缓存与多级缓冲 的区别:展示缓存可松,扣库存不可松。
降级与容灾
| 场景 | 策略 |
|---|
| 库存服务慢 | 停止进入下单,排队延长 |
| DB 主库故障 | 切换主从;已扣库存需人工/脚本对账 |
| 只读查询挂 | 关闭余票数字,保留「查询中」 |
| 全站过载 | 静态页 + 仅保留登录 |
设计检查清单(抢票)
| 检查项 | 问题 |
|---|
| 超卖 | 并发 1 万张票,能否卖出 10001 张? |
| 展示 vs 扣减 | 页面余票是否直接 DECR 展示 key? |
| 幂等 | 用户连点「提交」是否扣两次? |
| 支付回滚 | 超时未付是否释放库存? |
| 热点 | 春运 G/D 字头车次是否单点? |
| 公平 | 无限流时机器是否占满令牌? |
相关