12306 火车票系统核心难点详解
·
一、区间票库存模型(最核心难点)
问题本质
普通电商:iPhone 库存 100 台,卖一个减一个,简单。
火车票完全不同。假设一列车有 1000 个座位,途经 5 个站:
A → B → C → D → E
如果某人买了 A→E 的全程票,占了 1 个座位,那么:
- A→B 剩余 999
- B→C 剩余 999
- A→C 剩余 999
- C→E 剩余 999
- ……所有包含该座位经过区间的组合,余票都要减少
一条有 n 个站的线路,可售区间组合数为 n×(n-1)/2。京沪高铁 24 个站,就有 276 种区间组合。
数据建模方案
方案一:区间段位图法(12306 实际采用的核心思想)
站点:A(0) → B(1) → C(2) → D(3) → E(4)
区间段:[A-B], [B-C], [C-D], [D-E] 共 4 段
每个座位用一个 bitmap 表示占用情况:
座位 #1: 0000 → 全空闲
座位 #1: 1111 → 全程已售(A→E)
座位 #1: 1100 → A→C 已售,C→E 可售
查询余票(A→C)的逻辑:
遍历所有座位,检查 bitmap 的第 0、1 位是否都为 0
统计满足条件的座位数 = A→C 的余票数
售票(A→C)的逻辑:
找到一个 bitmap 第 0、1 位都为 0 的座位
将其第 0、1 位置为 1
方案二:分段计数法(简化版)
维护每个最小区间段的剩余座位数:
seg[A-B] = 1000
seg[B-C] = 1000
seg[C-D] = 1000
seg[D-E] = 1000
查询 A→C 余票 = min(seg[A-B], seg[B-C]) = 1000
售出 A→C 一张:seg[A-B]--, seg[B-C]--
此时查询 A→E 余票 = min(seg[A-B], seg[B-C], seg[C-D], seg[D-E])
= min(999, 999, 1000, 1000) = 999
缺陷:min 值只是上界,实际可售数可能更少(因为不同区间可能占用了不同座位,但 min 计算假设了最差情况)。精确计算需要回到位图法。
核心矛盾
| 需求 | 冲突 |
|---|---|
| 精确余票 | 需要遍历座位级 bitmap,计算量大 |
| 高并发查询 | 要求毫秒级响应 |
| 实时更新 | 每卖一张票,影响多个区间的余票 |
实际解法
- 分层计算:热门查询走缓存(分段计数法的近似值),下单时才走精确位图
- 预计算:每次售票后,异步更新所有受影响区间的余票缓存
- 分车厢/席别隔离:将一列车拆成多个独立库存单元,降低锁粒度
二、高并发库存扣减一致性
流量规模
- 日常:几十万 QPS 查询,几万 TPS 下单
- 春运放票瞬间:百万级 QPS 查询,十万级 TPS 下单尝试
- 热门车次(如京沪高铁某班):单车次可能承受数万并发写
为什么比普通秒杀难
普通秒杀:
if stock > 0:
stock -= 1 # 只需一个原子操作
火车票扣减:
1. 检查 bitmap 中目标区间是否空闲
2. 选择一个合适的座位(可能要考虑连座、靠窗等)
3. 修改该座位的 bitmap(多个 bit)
4. 更新所有受影响区间的余票缓存
5. 生成订单
这不是一个原子操作,涉及 读-判断-写 的复合操作。
热点问题
假设:北京→上海 G1 次,1000个座位,放票瞬间 5 万人抢
如果用数据库行锁:
- 锁整列车 → 串行化,吞吐量崩塌
- 锁单座位 → 用户不指定座位,系统分配,热点集中在分配逻辑
- 锁区间段 → 需要同时锁多个段,可能死锁
实际解法
1. 内存预扣 + 异步落库
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 用户请求 │ ──→ │ 内存(Redis) │ ──→ │ 数据库 │
│ │ │ 原子预扣 │ │ 异步持久化 │
└─────────────┘ └─────────────┘ └─────────────┘
- Redis 中维护库存的原子操作(Lua 脚本保证原子性)
- 预扣成功 → 发消息到 MQ → 异步写入数据库
- 预扣失败 → 直接返回无票
2. 分桶 + 合并扣减
将 1000 个座位分成 10 个桶,每桶 100 个座位
10 个并发请求可以分别打到不同桶,互不干扰
3. 排队串行化(最终方案)
对同一车次的扣票请求,进入单线程排队处理:
- 用内存队列保证顺序
- 单线程内无锁操作 bitmap
- 吞吐量靠多车次并行 + 单车次内批量处理
这类似 LMAX Disruptor 的思想:与其让多线程抢锁,不如单线程顺序处理,反而更快。
三、瞬时流量削峰
流量特征
┌────────────────────────────────────────┐
│ │
│ ▂▃▅▆██████▆▅▃▂ │
│ ▁▁▁▁▁▁▁ ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ │
│ │
│ ←───── 早8:00放票,流量在1-2秒内达峰 ──→ │
└────────────────────────────────────────┘
峰值/均值 比可达 100-1000 倍
多层削峰架构
第1层:客户端
├── 验证码(拖延 1-3 秒)
├── 前端排队动画(心理缓冲)
└── 客户端限流(按钮置灰 5 秒)
第2层:CDN / 网关
├── 静态资源缓存
├── IP 限流
└── 无效请求拦截(未登录、参数错误)
第3层:应用层
├── 令牌桶:提前发放有限令牌,无令牌直接拒绝
├── 本地缓存:余票为 0 的车次直接拦截
└── 随机丢弃:超出处理能力时,随机拒绝部分请求
第4层:服务层
├── 消息队列异步化
├── 请求合并(相同查询合并为一次 DB 访问)
└── 批量处理(攒一批扣减请求一次性处理)
第5层:数据层
├── 读写分离
├── 分库分表
└── 热点数据内存化
令牌机制详解
放票前 1 分钟:
1. 系统根据车次余票数,生成等量令牌(如 1000 张票 → 发 2000 个令牌)
2. 用户点击购票 → 先申请令牌
3. 拿到令牌 → 进入真实扣票流程
4. 未拿到令牌 → 返回「排队中」或「候补」
效果:将 10 万并发降为 2000 并发进入核心逻辑
四、选座与分配算法
目标冲突
| 目标 | 说明 |
|---|---|
| 用户满意 | 靠窗、连座、与同行人相邻 |
| 收益最大化 | 尽量不把长途座位卖给短途旅客 |
| 公平性 | 先到先得 vs 长途优先? |
| 计算效率 | 毫秒级决策 |
连座问题
3人同行购票 A→C,需要找 3 个相邻座位都满足 [A-B][B-C] 空闲
车厢座位布局:
窗 | 01 02 03 | 过道 | 04 05 | 窗
窗 | 06 07 08 | 过道 | 09 10 | 窗
...
需要在 bitmap 矩阵中搜索:
- 同一排
- 连续 3 个座位
- 对应区间段都为 0
这是一个 约束满足问题(CSP),在高并发下要快速求解。
长短途冲突
场景:A→B→C→D,座位只剩 1 个
请求1:A→B(短途,占 1 段)
请求2:A→D(长途,占 3 段)
如果先来先得,短途可能先把座位占了,
导致长途旅客买不到票(收益损失更大)
策略:
- 放票初期:优先分配给长途
- 开车前若仍有余票:释放给短途
- 候补队列中:长途优先级更高
五、候补购票
核心逻辑
候补队列:
├── 用户A:想买 北京→上海,1张,一等座
├── 用户B:想买 北京→南京,2张,二等座(要连座)
├── 用户C:想买 南京→上海,1张,二等座
└── ...
触发事件:
- 有人退票(释放了某个座位的某些区间段)
- 有人改签(释放旧区间,占用新区间)
- 系统释放预留票
匹配过程:
1. 退票释放:座位 #500 的 [北京-南京] 段
2. 扫描候补队列:
- 用户A 需要 [北京-上海] → #500 的 [南京-上海] 仍被占,不匹配
- 用户C 需要 [南京-上海] → 不匹配(释放的是北京-南京)
- 但如果同时有另一个退票释放了 #500 的 [南京-上海]...
3. 需要实时组合匹配,复杂度高
难点
- 部分匹配:退票释放的区间可能只覆盖候补需求的一部分
- 组合爆炸:多个退票组合起来才能满足一个候补
- 多人连座:不仅要区间匹配,还要座位相邻
- 实时性:退票后需要秒级完成匹配并通知用户
- 公平性:先候补的优先,但长途可能优先级更高,如何平衡?
六、分布式事务
一次购票涉及的操作
1. 库存服务:扣减 bitmap
2. 订单服务:创建订单记录
3. 座位服务:锁定具体座位号
4. 用户服务:记录购票信息
5. 支付服务:发起支付(30分钟超时)
6. 通知服务:发送短信/推送
异常场景
场景1:扣了库存,创建订单失败 → 需要回滚库存
场景2:订单创建成功,支付超时 → 需要释放座位 + 回滚库存
场景3:支付成功,通知失败 → 订单仍有效,通知可重试
场景4:网络分区,库存服务和订单服务不可达 → ?
解法:Saga + 最终一致性
正向流程:
预扣库存 → 锁座位 → 创建订单 → 等待支付 → 确认出票
补偿流程(任一步失败):
释放库存 ← 解锁座位 ← 取消订单 ← 退款
超时机制:
- 未支付订单 30 分钟自动取消
- 定时任务扫描异常状态订单,执行补偿
- 对账系统每日核对库存与订单一致性
七、总体架构概览
┌──────────────┐
│ CDN + WAF │
└──────┬───────┘
│
┌──────┴───────┐
│ 网关集群 │ 限流/鉴权/路由
└──────┬───────┘
│
┌────────────────┼────────────────┐
│ │ │
┌──────┴──────┐ ┌─────┴─────┐ ┌──────┴──────┐
│ 查询服务 │ │ 下单服务 │ │ 候补服务 │
│(读,可水平扩)│ │(写,需串行)│ │(异步匹配) │
└──────┬──────┘ └─────┬─────┘ └──────┬──────┘
│ │ │
┌──────┴──────┐ ┌─────┴─────┐ │
│ 多级缓存 │ │ 消息队列 │←────────┘
│L1本地+L2Redis│ │(削峰排队) │
└──────┬──────┘ └─────┬─────┘
│ │
└────────┬───────┘
│
┌──────┴───────┐
│ 库存核心引擎 │ 单车次单线程 bitmap 操作
└──────┬───────┘
│
┌──────┴───────┐
│ 分库分表 DB │ 按车次分片
└──────────────┘
八、总结:为什么 12306 是中国最难的系统之一
| 维度 | 普通电商秒杀 | 12306 |
|---|---|---|
| 库存模型 | 数量 -1 | 区间 bitmap,多维关联 |
| 热点程度 | 单品热点 | 单车次 + 单区间双重热点 |
| 峰值特征 | 可预期,持续数分钟 | 放票瞬间 1-2 秒内爆发 |
| 一致性要求 | 最终一致可接受 | 座位不能超卖,强一致 |
| 业务约束 | 简单 | 连座、长短途、候补、改签 |
| 用户规模 | 千万级 | 亿级注册用户 |
| 社会影响 | 商业损失 | 民生问题,容错率极低 |
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)