资讯详情

资讯详情

从12306看高并发系统设计:查询、扣票与限流的极致实践

每年春节前两个月我身边总有一群非程序员的朋友开始哀嚎“抢不到票”。他们经常会问我一个很有意思的问题“你们程序员这么厉害为什么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那种体量的系统但这些设计思想是可以迁移的。把它们的原理吃透哪怕只是应用在我们自己负责的某个小模块上也足够让系统质量提升一大截了。这大概就是研究优秀系统的价值所在。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →