每年春节前两个月,我身边总有一群非程序员的朋友开始哀嚎“抢不到票”。他们经常会问我一个很有意思的问题:“你们程序员这么厉害,为什么12306还会崩?为什么我刷一下就卡死了?”
说实话,这个问题在程序员圈里挺容易引起讨论的。很多外行以为12306只是个“卖票网站”,但在我看来,它可能是国内在互联网高并发领域,真正遇到过最极端场景的系统之一。甚至可以说,从纯技术的角度去解剖12306,你会发现在很多维度上,它的复杂程度是超过我们日常接触的绝大多数互联网应用的。
今天我想混着这些年看过的架构分析,以及我自己做高并发系统的一些实践经验,从一个程序员的视角,把12306背后那些“牛逼”的地方拆开聊一聊。不是为了吹捧,而是真心觉得,这套系统设计里有很多值得我们反复研究的思路。
1. 没经历过“候补排队”,你根本不懂12306的流量峰值
聊技术之前,得先把12306面临的场景说清楚,不然你说它牛逼,很多人会觉得你在扯淡——“不就查个票卖个票吗?”
我们可以拿大家最熟悉的电商促销来对比。每年双十一,很多电商平台的流量峰值大概是多少呢?几十万到几百万的QPS(每秒请求数),这已经很吓人了。但电商有一个巨大的优势:它的核心商品是“无限库存”或“高库存”的。你买一件衣服,库存有一万件,那一万个人同时下单理论上都能成功,系统压力主要在“写订单”上,而“扣库存”可以非常轻量地处理。
12306不一样,它做的是什么?它是一个极端严格的库存系统,而且库存是“强语义”的。
- 一张票,从北京到上海,它不是一个独立商品,它关联的是整个线路的运输能力。
- 每一个座位就是一个物理实体,不能超卖。
- 一张全程票被买走,意味着沿途所有区间站的票额池都要联动变化。
这意味着什么?意味着12306不能像电商一样先收单再慢慢锁库存,它必须在用户提交订单的那一瞬间,完成极其复杂的“余票实时计算”和“座位分配”。
再来看看流量。2024年春运期间,12306的日访问量峰值达到了多少呢?根据公开报道的一些数据,高峰期网站日访问量一度超过2000亿次量级(包含静态资源请求),车票开售瞬间的“秒杀级”并发请求,峰值可以冲到每秒百万级别以上的请求量。这个数字放在全球互联网来看,都是非常罕见的场景。
更变态的是,这种流量不是均匀分布的,它集中在每天的几个放票时间点(比如早上8点、10点、12点)。到时候,几千万人同时在手机前等着开抢。这种瞬间冲击,和电商大促抢购相比,有过之而无不及。
所以,当有人问我“12306为什么不搞个高性能数据库把票存里面,再加个Redis做缓存,不就行了吗?”,我就知道他把问题想简单了。
2. 核心难点拆解:12306到底难在哪里
要把12306的技术实力聊透,我们先拆一下它的核心业务链路。一个用户从查询余票到最终买到票,大概经历这么几个环节:
- 查询余票:用户输入出发站、到达站、日期,系统要在秒级内返回所有可选车次、各席别余票数。
- 提交订单:用户选好车次席位,点击提交,系统要完成“余票校验、锁定座位、生成待支付订单”。
- 支付处理:用户在30分钟内完成支付,系统确认支付结果、正式出票。
- 候补逻辑:没票了可以候补,系统要监控退票、改签产生的动态余票,按候补顺序自动分配。
这几步听起来简单,但每一步对技术方案的要求都是“地狱级”的。
2.1 余票查询:你是“有脑子的”,不是查数据库的
为了把查询和交易隔离,12306采用了一个非常经典的做法:把“余票查询系统”和“交易系统”完全分离。查询服务绝对不能直接打数据库。
你每次打开12306查车票,看到的余票数,其实不是实时从数据库里读取的,而是从内存缓存里读取的。但牛逼的地方不在这,牛逼在于它的缓存结构设计。
做过高并发系统的朋友都知道,如果缓存里存的是“查询结果”,那么每个不同查询条件都会产生一个缓存Key。比如北京到上海,和北京到南京,就是两个Key。如果车次多、线路多,缓存Key的数量会非常恐怖。
12306的做法是,把余票数据按“车次+日期”的粒度做内存计算。比如一个车次一天的数据,是一组基础数据,包括各站间的剩余座位情况。当用户查询北京到上海时,系统不是直接找一个现成的结果,而是动态根据该车次的座位分布数据,实时计算这条线路还有多少票。计算过程发生在内存里,纯CPU计算,极快。
有点像一个提前排好的积木矩阵,查询的时候只做一次“局部求和”,而不是全表SQL扫描。这种方式,既保证了查询灵活度,又减少了缓存Key的爆炸问题。
但就算这样,查询量依然巨大。12306还有一套多级缓存策略,比如像中间列的列车时刻表这类变动极少的数据,缓存时间可以拉长;余票数据缓存时间可能只有几秒;而像车次列表这类静态信息,甚至可以放到CDN,由前端直接读取。
2.2 最难的“扣票”逻辑:一票背后是一个复杂的空间占用问题
如果说查询是“数量大”,那么下单扣票就是“逻辑深”。
这里有个关键概念:一张票不只是从A站到B站那么简单。我们设想一下,如果一列火车有10个站:
- 用户A买了北京到上海(假设是中间站)的票,那么这个座位在北京-上海区间段内就不可再售。
- 但它后面,上海-终点站之间,如果用户A不坐了,这段区间理论上还可以再卖一个人。
也就是说,一个座位在物理上是一整个线性区间,但在售卖逻辑上,它可能被切成多个不相交的区段分别售出。这就引出了经典的“重叠区间匹配”问题。
用户下单时,系统要做的就是:在某个车次上,找到一个座位,它在你买的那一段区间内是“空闲”的;同时如果用户选的是卧铺,还要考虑上下铺的分配策略,比如先卖下铺、再卖中铺、最后卖上铺这种规则。
在数据库层面,如果简单地用一行数据表示“这个座位这个区间有没有被占用”,一旦多人同时抢,就必然面临大量行锁竞争。高峰期几万个请求同时要改同一趟车的数据,这是恐怖的行锁等待。
为了解决这个问题,12306做过一次非常重要的架构升级,就是“按区间预分配+内存队列扣减”的模式。系统把整趟车的售票能力,在内存中构建成一种特殊的“座位区间树”或类似的数据结构,下单时直接在这个数据结构上做原子操作,而不是数据库的行锁。
这相当于把高频的、冲突严重的“局部扣减”操作,从数据库搬到了内存中,通过更细粒度的锁机制(甚至无锁化)来处理。数据库只做最终的结果落库和账务核对。
这种设计思路,和我们平时处理“库存扣减”时的分段锁、缓冲队列有异曲同工之妙,但12306的复杂度要高得多,因为它涉及的不是一个数字的加减,而是一个空间分配问题。
2.3 分布式事务与最终一致性:不是“扣了就行”,还要“稳得住”
下单扣票之后,才是真正考验系统健壮性的地方。用户支付、系统出票,这之间涉及多个系统协作:订单系统、支付系统、票务库存系统、通知系统等。
如果用户付了钱,但出票失败怎么办?如果票扣了,用户没支付,票什么时候释放?
这些问题在架构上通常用“分布式事务”来解决。但真正的核心场景里,12306并没有完全依赖强一致性的分布式事务(比如两阶段提交)。因为那种方案在高并发下性能太差。
它选择了另一种思路:异步化 + 最终一致性 + 对账补偿。
我给你描述一下这个过程就明白了:
- 用户提交订单,系统先在内存中锁定座位,同时生成一个“待支付订单”并记录一个流水号。
- 用户完成支付后,支付系统回调订单系统,订单系统再将“已支付”事件发送到消息队列。
- 出票系统消费这个事件,将内存锁定的座位正式落库,生成车票信息。
- 如果消息丢失或处理失败,会有定时对账任务,去扫描那些“已支付但未出票”的异常订单,自动重试。
为了避免重复出票或票资不一致,整个链条加了大量的幂等控制。所谓的幂等,简单说就是“同一个操作执行一次和执行多次,结果是一样的”。比如回调通知可能因为网络原因发送了两次,订单系统要能识别出这是同一个支付结果,不能生成两张重复车票。
很多人会觉得这种异步化不就是“削峰填谷”嘛,有什么难的?难就难在,它既要保证用户体验(支付完成后很快出票),又要保证系统高吞吐,还要保证资金资产不混乱。这背后的状态机设计、消息可靠性保障、以及大量边缘场景的处理,都是实打实的工业级复杂度。
3. 从“可用性”角度看:高可用架构的教科书级实践
程序员评价一个系统牛不牛逼,除了看业务复杂度,还要看它在海量流量面前的“存活能力”。12306在这一点上的投入,可以说是不计成本的。
3.1 异地多活与流量调度
对普通人来说,服务器在哪根本不重要,但对平台来说,这是生死攸关的。如果数据中心部署在同一个城市,一旦发生电力故障或网络故障,全国购票就瘫痪了——这在春运期间是不可接受的。
所以12306的架构是典型的“两地三中心”甚至更强的多活架构。多个数据中心之间,通过专线网络进行数据同步。正常情况下,流量按照某种规则分配到不同中心;一旦某个中心异常,另一些中心可以紧急接管全部流量。
这种容灾能力的背后,不只是买几台服务器那么简单。它需要解决:
- 数据实时同步:订单、余票数据如何在多个中心间保持同步,不会出现“这个数据中心说没票,那个数据中心说有票”。
- 流量调度能力:DNS切换、HTTP重定向、路由策略,都要在分钟级完成,不能等人工去改配置。
- 读写分离与降级:比如查询服务和下单服务分离,万一某个中心的下单服务出问题,至少还能保证用户查得到票、看得到车次信息。
这套东西,不是单一技术能解决的,而是整个基础设施、运维体系、监控体系长期建设的结果。
3.2 服务降级与限流:不只是“拒绝”,而是“排队”
我们经常在网上看到有人吐槽12306出现“系统繁忙,请稍后重试”。外行觉得这是崩了,但程序员看到的是限流策略正在生效。
真正高并发的系统,设计目标从来不是“无限处理所有请求”,这不可能。设计目标应该是“在系统能力范围内,最大化处理有效请求,同时对超出的请求进行优雅控制”。
12306的限流就很有智慧。它不是简单粗暴地“丢弃请求”,而是引入了一个排队机制。你可以理解为,当同时抢票的人太多,系统不会告诉你“你失败了,别抢了”,而是告诉你“你已进入队列,正在等待分配余票”。
这个“排队”策略,底层就是一套优先级队列 + 超时控制机制。系统预测了未来一段时间内的处理能力,然后限制同时进入交易环节的用户数,其余用户全部在队列里等待。一旦前一个用户支付超时释放名额,队列后面的用户就顶上。
这种做法,比盲目拒绝请求体验好得多,也保证了系统核心交易链路不至于被瞬时流量打爆。
3.3 防“羊毛党”与抢票软件对抗
这个层面一般算作业务安全或风控,但12306的做法也很值得聊。每年抢票季,各种抢票软件、浏览器插件就会冒出来,利用自动化脚本模拟用户操作,以毫秒级速度刷票。
从技术角度讲,抢票软件和普通用户的竞争,本质上是“公平性问题”。抢票软件可以在放票前就不断刷新接口,甚至在放票瞬间就发起精确请求,这会让手动用户陷入劣势。
12306在这方面做过很多技术尝试:
- 图片验证码:早期用简单数字验证码,后来升级为复杂的图片点选验证码。虽然大家都很烦这个验证码,但从反自动化角度来说,它确实提高了脚本的识别门槛。
- 行为特征识别:检测用户的点击频率、操作间隔、设备指纹等。如果发现某个账号的请求行为异常,比如1秒钟内发起几十次查询,就会触发风险控制,要求额外验证甚至限制登录。
- 候补机制:这个功能其实是打击抢票软件的一记重拳。过去抢票软件的核心优势是“监控退票、第一时间捡漏”,但有了候补机制后,系统按候补顺序公平分配余票,所有用户只需排队等待,脚本的“手速优势”被大幅削弱。
从技术实现上看,行为风控依赖的是实时计算中的复杂事件处理引擎,要对海量用户行为数据进行实时分析、特征抽取和规则判定。这本身就是一套大数据技术的活。
4. 那些被误解的技术细节:为什么“上云”和“堆机器”不能解决一切
每次12306系统一出现小幅波动,网上就会有一堆人站出来说:“为什么不花点钱多买几台服务器?”“为什么不上云?”作为程序员,我特别想给这些说法泼点冷水。
4.1 核心瓶颈是“数据一致性”,不是“计算力”
如果你只是做查询,堆机器确实有效。加缓存、加负载均衡,把前端查询压力分散掉,能解决大部分问题。
但12306的核心交易逻辑,对数据一致性要求极高。几万个并发请求同时抢同一趟车、甚至同一个座位,这时候不是说你加多少台服务器都能解决的。因为无论多少台机器,最终对“这张票属于谁”的裁决,必须在一个节点上完成。这就是分布式系统里所谓的“串行点”。
这就好比你去银行取钱:无论银行开多少个窗口,最终系统里“账户余额”的变更,必须在一个统一的账本里进行。你不能因为排队人多,就复制几百个账本同时操作,那样会乱套。
12306为了优化这个串行点的性能,做了非常精细的拆分。比如把列车数据按照天、车次维度打散分布到不同服务器上,让不同车次的票务处理可以并行。但对同一车次的并发请求,总有一个上限和瓶颈。这种场景,单纯堆机器解决不了,必须靠更精细的数据结构优化。
4.2 云计算的弹性,受限于技术栈和历史包袱
有人说12306完全可以“全量上云”,用Kubernetes弹性扩容来应对流量。理论上是可行的,但现实是,这样体量的系统,涉及大量的存量系统、底层网络优化、定制化中间件,不可能一蹴而就完成迁移。
更重要的是,就算上了云,核心交易链路的数据库分片、一致性协议依然是绕不开的难题。云能提供的是“更灵活的资源调度能力”,但没法替你解决业务本身的数据约束。
所以说,那些觉得“12306用云就能搞定”的说法,是对大规模高并发系统设计难度的一种低估。每一套高并发系统,真正的技术含量都在“数据结构和调度策略”上,而不在“用了几个云服务”上。
4.3 “火车票库存”不是电商库存
再补充一个很容易被误解的点。以前有个很出名的段子说:12306如果把票放到电商平台卖,早已被秒杀系统架构解决了。
这个说法错在哪?错在它忽略了火车票的“区间重叠约束”。电商每个SKU的库存独立,库存量只是一个数字:卖一件减一。而火车票的库存是一个“区间可用性”问题。
为了让你更直观感受这个复杂度,我模拟一下:
假设一列火车从始发站到终点站,中间有5个停靠站:
- 如果有人买了始发站到终点站的全程票:这个座位所有中间区段都不能卖了。
- 如果有人买了第2站到第4站的票:那第1站到第2站、第4站到终点站还可以卖给其他人吗?可以的,前提是这两个区间的乘客上车和下车时间不重叠。
这个算法难点在于:你要在大量“区间已售记录”中,快速找到某个座位在某段区间是否被占用。简单遍历肯定不行,涉及大量计算。
所以12306的库存系统,本质是一套动态区间分配引擎。这比电商的“一个数字减一”复杂一个维度。
5. 结合我们的日常开发,能从12306学到什么
聊了这么多宏观架构,回到我们自己的日常开发,我觉得有几个思路是我们可以借鉴的。
5.1 提前梳理核心链路的锁竞争点
在做任何高并发设计之前,先别急着上缓存、上消息队列。先想清楚:你的系统里,哪个环节是必须串行的?哪些数据是强一致的?
比如我做过的模拟项目X,是一个抢票活动系统,一开始我也想着用Redis做分布式锁,把扣减库存的并发控制做掉。但后来发现,并发场景下就算加了锁,锁粒度设计不好,还是会有性能瓶颈。
12306的思路启发了我:把锁的粒度尽量缩小。比如不要锁“整个活动的库存”,而是把库存按照某种维度分片,比如分成100个片,每个请求先根据用户ID哈希到这个片,再在片内加锁。这样,锁冲突的概率就降低了100倍。
5.2 用“预计算”代替“实时计算”
余票查询系统的高性能,很大程度上得益于“预构建数据矩阵”。我们日常开发中,很多慢SQL其实也可以这样优化。比如报表系统的统计数据,完全没必要每次实时汇总,可以在凌晨空闲时预计算结果,然后查询时直接读结果。
5.3 异步化 + 对账,是分布式系统的安全网
在微服务架构中,完全避免分布式事务是不可能的,但我们完全可以尽量减少对强事务的依赖。像订单状态流转、通知发送、积分变更这类场景,用消息队列异步解耦 + 定时对账补偿,往往会比强行用分布式事务框架性能和稳定性都更好。
这招我在几个项目里都验证过。只要消息中间件的可靠性有保障(比如开启生产者确认、消费者手动ACK、死信队列),异步化并不会带来不可控风险,反而能大幅提升系统吞吐量。
5.4 把体验“藏”在工程方案里
12306的排队机制给我最大的启发是:限流不一定等于拒绝。很多时候,用户能接受“等待”,但不能接受“失败”。
我们在做秒杀系统时,也借鉴了这种思路:用户点击抢购后,不是立即返回成功或失败,而是先返回一个排队ID,前端轮询查询排队状态,服务端在后台按顺序处理。这种设计在体验上比“秒失败”好太多了,而且能有效保护后端系统。
6. 几个值得关注的细节:工程化思维的闪光点
聊一些容易被忽略的细节,这些细节恰恰是最见功力的地方。
第一点是放票时间的错峰设计。12306并不是所有车票都在同一秒放出,而是不同车站、不同车次分批放票。这个策略从技术上讲,实质上是把瞬时流量峰值“削平”,让系统在更长时间段内均匀承接压力。这个思路其实也广泛应用于各种大促活动,比如电商平台的“分批抢购”。
第二点是候补机制的架构巧妙。候补订单本质上不是一个新的抢票通道,而是把“退票监控”这件事从“人肉刷”变成了“系统代刷”。这个功能上线之后,用户不需要再不断刷新页面,服务器也减少了大量无效的查询请求。这对系统负载的优化,起到了明显作用。它把一个“高频轮询”问题,变成了一个“低频异步任务处理”问题。
第三点是多端协同的技术支撑。现在的12306,网页、手机App、小程序多端并行,用户在不同端上的操作要做到实时同步。比如你在电脑上买了一张票,手机上马上就能看到。这个体验背后,是消息推送、分布式缓存、会话统一等多套技术体系在协同工作。
第四点是自动改签、换乘方案的算法能力。当你购买的车票出现变化需要自动改签时,系统要从多个车次中帮你挑选可用方案,并按价格、时长、中转次数等条件排序。这个过程涉及路径规划和动态规划的算法,虽然看起来不像抢票那么“惊心动魄”,但也是工程师们长期优化的成果。
7. 写在最后:不要用“抢不到票”来否定一个系统
我做程序员这些年,有一个越来越深的体会:评价一个系统的技术水平,不能只看它“好不好用”,还要看它承载了什么级别的复杂度。
12306确实不算完美,它也有过排队时间过长、高峰期卡顿、操作响应慢等体验问题。但如果有人因为这些体验问题,就认为它“技术上不牛逼”,我是不太认同的。
换个角度想:每年春运高峰期,几千万人同时涌入系统买票,要在极短时间内完成大量强一致性交易,还要保证资金准确、席位不冲突——这不是随便哪家互联网公司都能搭建起来的能力。全世界范围内,能稳定扛住这种量级的交易系统,掰着指头数得过来。
我个人在实际项目中,每当遇到高并发场景,都会习惯性地回忆一遍12306的架构思路:把查询和交易分流、把锁粒度做细、用异步替代同步、用队列替代直接拒绝。这套方法论,几乎在所有“瞬间高并发+强一致性”的场景里都适用。
也许我们普通程序员一辈子也接触不到12306那种体量的系统,但这些设计思想是可以迁移的。把它们的原理吃透,哪怕只是应用在我们自己负责的某个小模块上,也足够让系统质量提升一大截了。这大概就是研究优秀系统的价值所在。