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 / 订单幂等键
  • 重复提交返回同一订单号,不二次扣库存

热点车次与分片

问题方案
单车次库存热 keyRedis 分 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 字头车次是否单点?
公平无限流时机器是否占满令牌?

相关