一、区间票库存模型(最核心难点)

问题本质

普通电商: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 秒内爆发
一致性要求 最终一致可接受 座位不能超卖,强一致
业务约束 简单 连座、长短途、候补、改签
用户规模 千万级 亿级注册用户
社会影响 商业损失 民生问题,容错率极低
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐