12306 系统抢票原理
12306 是全球规模最大的实时交易系统之一,春运期间日 PV 超 500 亿,日售票量超 2000 万张,峰值 QPS 达数十万。理解它的设计,是学习高并发系统架构的极佳素材。
整体架构
公私有云分流
系统面临的核心矛盾:查询量远大于交易量(查询占比超 75%),但交易的安全性要求更高。
| 子系统 | 部署位置 | 原因 |
|---|---|---|
| 查询系统(余票查询、车次查询等) | 公有云 | 读多写少,弹性扩容方便,应对春运峰值 |
| 核心交易系统(购票、支付、退票等) | 私有云 | 涉及资金与用户隐私,安全性要求高 |
| 用户系统 | 私有云 | 敏感数据,自主管控 |
- 公有云可在春运期间快速扩容,平时缩容节约成本
- 核心交易走私有云,保证数据安全与合规
微服务拆分
12306 将系统拆分为多个独立服务:
- 用户服务、车次服务、余票服务、订单服务、支付服务、候补服务、退票服务等
- 每个服务可独立扩缩容,互不影响
- 服务间通过消息队列异步通信,降低耦合
余票计算:位图方案
问题:车票组合爆炸
一趟经停 N 站的列车,车票区间组合数为 N × (N-1) / 2。一趟北京→上海经停 5 站的列车就有 15 种区间票。全路网每天数千趟列车、上万种区间,传统数据库关系表的数据量和锁竞争都会爆炸。
位图方案
核心思想:用二进制位表示每一段是否有空座,位运算判断区间是否有余票。
以一辆 3 站列车(A→B→C)为例,座位被分成 2 段:
段1: A→B 段2: B→C
位图: [1] [1] ← 每一位代表一段是否空闲(1=空闲,0=已售)
买 A→C 全程:段1 + 段2 相加,不溢出则有票
买 A→B 区间:段1 为 1,有票
买 B→C 区间:段2 为 1,有票判断逻辑:将目标区间涉及的所有段的位相加,任何一段溢出(即该段位图中所有座位的该段都已售出),则无余票。
位图方案的优势
| 对比项 | 传统数据库 | 位图方案 |
|---|---|---|
| 数据量 | 组合爆炸,N 站列车有 O(N²) 条记录 | 只需 N-1 个段 × 座位数,数据量 O(N) |
| 并发锁 | 行级锁,高并发下锁等待严重 | 位运算可做到原子操作,减少锁粒度 |
| 查询速度 | 多表 JOIN,慢 | 位运算,极快 |
| 内存占用 | 大 | 紧凑,适合缓存 |
位图计算推测是原子性的——一个座位一个座位算,避免并发冲突。
全程优先与区间限售
问题:全程票有余,区间票售罄
同一趟列车,为什么全程票还有,但中间某段区间票却先卖完了?
如果中间热门段被短途旅客占满,全程旅客就无法买到票,导致列车末端座位空跑,运力浪费。
解决方案:全程优先,区间限售
- 开售时优先给全程区间放票,短程票只放少量或不放
- 随着时间推移逐步给中间区间放票
- 越靠近发车时间,短程票越多,抢到票的概率越大
- 一开始抢不到票不一定是票被抢完了,而是票放得少
考量因素
| 视角 | 逻辑 |
|---|---|
| 铁路系统 | 保障整体运力最大化——座位都坐满别空运,旅客周转量最大化 |
| 公平性 | 长/短途旅客是零和博弈,应优先保障选择权较少的旅客需求 |
| 短途旅客 | 短途有更多替代出行方式(大巴、自驾等),长途选择有限 |
放票时间窗口
- 车票通常提前 15 天开售
- 不同区间分时段放票,而非一次性全部放出
- 系统会根据历史数据动态调整各区间放票比例
候补机制
问题:买不到票的焦虑
区间限售让大部分人无法第一时间抢到票,且不确定性造成强烈焦虑。
候补购票流程
用户提交候补 → 预付票款 → 进入候补队列 → 有票时按排队顺序自动分配 → 通知用户
↑
退票池优先匹配候补 → 匹配成功 → 自动购票- 候补是先交钱排队,有票自动购买,无需反复刷新
- 可设置截止候补时间,到期未成功自动退款
- 属于帕累托改进——没人利益受损,候补者获益
候补的系统性价值
| 维度 | 作用 |
|---|---|
| 信息收集 | 系统通过候补数据感知哪里缺票、缺多少,从而决定是否加开列车 |
| 退票优先匹配 | 退票先进入退票池匹配候补,而非流入公开市场——第三方爬虫抢不到 |
| 打击黄牛 | 后续公开市场只剩候补消耗不完的票,黄牛可抢的票极少 |
| 优先级保障 | 候补乘客优先级高于所有抢票脚本——脚本再快也在候补之后 |
候补机制之前,系统不知道缺票的精确分布,也无法科学决策是否加开列车。候补让供需信息变得透明。
高并发应对
缓存策略
- 多级缓存:浏览器缓存 → CDN 缓存 → 本地缓存 → 分布式缓存 → 数据库
- 余票数据读多写少,天然适合缓存
- 购票时先扣缓存再异步写库,提高响应速度
- 缓存与数据库的一致性通过消息队列保证
数据库分库分表
- 按车次/日期分片,将流量分散到多个数据库实例
- 订单表按用户 ID 哈希分表
- 读写分离:查询走从库,交易走主库
限流与降级
- 限流:接口级别的 QPS 限制,防止雪崩
- 排队:购票请求先进入消息队列,异步处理,削峰填谷
- 降级:极端情况下关闭非核心功能(如注销查询、广告位),保障核心购票链路
- 验证码:防止机器人刷票,增加请求成本
异步处理
- 购票请求 → 消息队列 → 异步处理 → 通知结果
- 用户提交后无需同步等待,避免连接长时间占用
- 候补、退票匹配等均为异步任务
防黄牛与公平性
多层防御体系
| 层级 | 措施 |
|---|---|
| 前端 | 验证码(滑块/点选)、请求频率限制、设备指纹识别 |
| 接入层 | IP 限流、异常流量识别、WAF 防护 |
| 业务层 | 实名制认证、人证比对、同一身份限购 |
| 候补层 | 退票优先候补、公开市场余票极少、脚本优先级最低 |
| 数据层 | 请求去重、防重复提交、分布式锁 |
实名制 + 人证比对
- 购票需实名,进站需人证比对
- 同一身份证同一车次限购一张
- 彻底杜绝了"屯票倒卖"的可能——黄牛买得到但用不了
验证码的演进
- 早期:简单数字验证码 → 被 OCR 轻松破解
- 中期:图片点选(找所有红绿灯等)→ 提高机器识别难度,但也误伤用户
- 现在:行为验证(滑块、轨迹分析)→ 兼顾安全与体验
技术栈概览
| 层级 | 技术选型 |
|---|---|
| 前端 | Vue + Nginx + CDN |
| 网关 | Spring Cloud Gateway + 限流 |
| 服务 | Spring Cloud / Spring Boot 微服务 |
| 缓存 | Redis Cluster(余票缓存、分布式锁) |
| 消息队列 | RocketMQ / Kafka(异步处理、削峰) |
| 数据库 | MySQL 分库分表(ShardingSphere) |
| 搜索 | Elasticsearch(车次查询) |
| 监控 | Prometheus + Grafana |
关键启示
- 读多写少的系统,缓存是最好的武器
- 组合爆炸问题,往往需要换一种数据结构(如位图)才能解决
- 高并发不等于高性能——限流、排队、降级是比"更快"更重要的策略
- 公平性设计本身就是技术问题——候补机制用信息透明替代了零和博弈
- 系统架构没有银弹——公私有云混合、同步异步结合、缓存与数据库并存,都是权衡