公司动态

从12306系统拆解高并发架构:分布式、缓存与锁的实战设计

📅 2026/8/13 4:11:28
从12306系统拆解高并发架构:分布式、缓存与锁的实战设计
1. 项目概述从“抢票难”到理解一个国家级系统的复杂性又到了一年一度的出行高峰季朋友圈里“求加速包”、“助力抢票”的链接又开始刷屏了。作为一个技术人看着大家为了一张火车票焦头烂额我总会想起几年前自己第一次尝试去理解“12306”这个系统时的震撼。它远不止是一个简单的购票网站或APP而是一个承载着全球最庞大、最复杂瞬时并发访问需求的在线交易系统之一。今天我想把自己当初学习、拆解这个“铁路购票系统”的笔记和心得整理出来尤其是上半部分关于系统核心挑战、架构演进和并发处理逻辑的内容。这不仅仅是为了应付面试或技术讨论更是为了理解在极端业务场景下一个合格的后端架构应该如何思考和设计。无论你是正在学习分布式系统的大学生还是对高并发处理感兴趣的后端工程师希望这篇笔记能帮你拨开迷雾看到“抢票”背后那个波澜壮阔的技术世界。2. 核心挑战与业务场景深度解析2.1 独一无二的业务峰值与数据强一致性要求理解12306首先要理解它面临的业务场景有多么特殊。这和我们日常开发的电商秒杀、演唱会抢票有本质区别。电商秒杀的商品库存可能是几千、几万而12306在春运期间面对的是数亿人次在短时间内对全国铁路网络上百万个座位更精确地说是“席位”一个座位是一段旅程的一个席位的查询和抢占。这个“短时间”往往以毫秒计尤其是在放票瞬间。这就引出了第一个核心挑战极端的瞬时并发。想象一下数千万甚至上亿的用户在同一个时间点例如早上8点整点击“查询”或“提交订单”这对后端服务造成的请求洪峰是毁灭性的。其次是库存的强一致性与实时性。火车票的库存不是简单的数字减一。它涉及复杂的席位复用计算。一张从北京到广州的火车票中途可以被拆分为“北京-武汉”、“武汉-广州”等多个区段售出。系统必须保证在任意时刻同一个座位在同一段旅程上不会被重复售出。这要求库存数据席位状态必须是全局强一致的任何一次成功的扣减都必须立即、准确地同步到所有查询节点不能出现超卖。这与许多互联网业务采用的“最终一致性”思路完全不同在这里数据不一致就意味着重大事故。2.2 从“IOE”到分布式云架构的演进之路早期的12306系统和许多传统企业核心系统一样构建在IBM小型机、Oracle数据库和EMC存储即IOE架构之上。这套架构稳定、可靠但扩展性极差且成本高昂。在春运海量并发面前IOE架构的瓶颈很快显现集中式的数据库根本无法处理每秒数十万级的写请求下单、占座。因此12306的架构演进是一场经典的“去IOE”和互联网化改造。其核心思路是读写分离、数据分片、异步化和缓存化。读写分离将查询请求和下单请求分离。查询是读多写少且可以容忍一定的数据延迟比如几秒钟内的席位缓存而下单写库存则要求强一致。通过将查询流量导向独立的读集群极大减轻了核心交易数据库的压力。数据分片这是解决数据库写瓶颈的关键。火车票的库存数据不再是集中存放在一个巨大的数据库表中而是按照某种维度例如车次日期进行分片分布到多个数据库实例上。这样对一个车次库存的扣减操作只会落到其中一个数据库分片上将全局的写压力分散开来。异步化并非所有步骤都需要同步实时完成。例如用户提交订单后生成订单、占座、支付这几个步骤可以解耦。系统可以先快速响应用户“占座成功”生成一个待支付的订单然后将后续的席位确认、库存最终扣减等操作放入消息队列异步处理从而缩短用户端的等待时间提升系统吞吐量。缓存化这是应对海量查询的法宝。大量的余票查询、车次信息、站点数据都是相对静态或变化不频繁的可以缓存在Redis等内存数据库中。12306构建了多级缓存体系从用户浏览器本地缓存、CDN缓存到应用层缓存如Redis集群层层过滤最终到达数据库的查询请求已经大大减少。注意这里的分片策略是关键中的关键。分片维度选择不好会导致“数据倾斜”即某些分片压力巨大如热门车次而其他分片空闲。实践中可能需要结合车次、日期、出发站等多种因素进行复合分片甚至需要动态调整。3. 核心技术点拆解如何扛住春运流量3.1 余票查询与库存计算模型余票查询是12306最频繁的操作也是技术难点。它的复杂性在于你查的“北京到上海”的票并不是一个独立的库存而是由沿途所有区段的席位占用情况组合计算出来的。系统内部维护的是一张“席位状态表”记录每个座位在每一段行程区间的占用情况。当用户查询A站到B站的余票时系统需要找出所有经过A站和B站的车次。对于每个车次遍历所有座位或席位。检查该座位在A到B这个区间的每一个“子区间”相邻两站之间是否都未被占用。统计所有符合条件的座位数量即为余票。这个过程如果实时扫描数据库计算在高峰期根本不可能完成。因此实时计算缓存是必然选择。一种常见的优化方案是采用“位图”或“二进制串”来表示一个席位的占用情况。例如一个车次有1000个座位从起点到终点有20个站那么可以形成一个1000x20的二维位图每个位0或1代表该座位在某个区段是否被售出。查询时通过位运算如AND操作可以快速判断一个区间是否全部为空。这个位图可以预先计算好并缓存在内存中查询时直接进行内存计算速度极快。3.2 高并发下单与锁的设计当用户点击“提交订单”时系统进入最核心、最脆弱的环节。这里的关键是处理“超卖”即同一座位在同一区间被重复售出。在分布式环境下这需要分布式锁或更精细的并发控制机制。单纯使用数据库的行锁如SELECT ... FOR UPDATE在每秒数十万下单请求面前会立刻死锁或成为性能瓶颈。12306采用的是一种“异步排队数据库乐观锁”的组合拳。请求排队与削峰用户点击提交后请求并不直接冲击库存数据库而是先进入一个分布式消息队列如RocketMQ、Kafka。这个队列起到了缓冲和削峰的作用将无序的、海量的瞬时请求变成有序的、匀速处理的流。异步处理与库存扣减后端的订单处理服务从队列中顺序消费消息。进行库存扣减时采用“查询-判断-更新”的模式并利用数据库的乐观锁机制例如更新时带上版本号或原始库存数作为条件。伪代码逻辑如下-- 假设有一张席位库存表 seat_inventory -- 有字段seat_id, journey_segment, status, version BEGIN TRANSACTION; -- 1. 查询当前席位的状态和版本号 SELECT status, version FROM seat_inventory WHERE seat_id ? AND journey_segment ? FOR UPDATE; -- 2. 判断状态是否可用如‘空闲’ -- 3. 如果可用尝试更新用version作为条件防止并发更新 UPDATE seat_inventory SET status 已占用, version version 1 WHERE seat_id ? AND journey_segment ? AND version ?; -- 如果影响行数为1表示扣减成功为0则表示已经被其他请求修改扣减失败。 COMMIT;结果异步通知扣减成功或失败后处理服务将结果通过另一个通道如WebSocket、长轮询通知给用户前端。用户看到的是“排队中” - “占座成功”或“失败”的流程。这种设计将同步的强一致性压力转化为异步的最终一致性流程虽然增加了系统复杂性但换来了吞吐量的指数级提升。3.3 分布式缓存与静态资源优化面对海量的读请求缓存的设计决定了系统的响应速度和生存能力。12306的缓存体系是立体化的客户端缓存利用HTTP缓存头让浏览器缓存静态资源JS、CSS、图片甚至部分不常变的页面数据。CDN缓存将全国的静态资源甚至动态生成的、用户个性化的页面片段通过ESI等边缘包含技术推送到离用户最近的CDN节点。应用层缓存Redis集群这是主力。缓存的数据包括车次时刻表、站点信息几乎不变缓存时间很长。余票查询结果这是热点。但缓存时间很短可能只有几秒到一分钟因为库存变化很快。需要非常精细的缓存失效策略一旦有订单成功必须及时清除或更新相关车次、席位的缓存。用户会话信息用户登录状态、购物车信息等。实操心得缓存是一把双刃剑。对于余票这种强实时数据缓存时间设置太短起不到保护数据库的作用设置太长又会卖“过期”的票导致用户下单失败体验差。一个折中的方案是采用“多级缓存主动更新”策略。例如第一级缓存本地缓存时间很短5秒第二级缓存Redis时间稍长30秒但当下单成功时系统会主动发送消息让相关缓存立即失效。4. 系统架构与组件选型推演4.1 总体架构分层设计基于上述挑战和解决方案我们可以勾勒出一个简化版的12306后端架构分层图。请注意这是基于公开资料和技术推演的模型并非真实架构。接入层负责流量接入和初步负载均衡。使用Nginx/OpenResty集群通过Lua脚本实现一些简单的逻辑如限流针对同一IP的频繁查询、请求过滤防爬虫、SSL卸载等。这一层的目标是快速处理、快速分发将无效或恶意请求挡在门外。应用服务层这是业务逻辑的核心采用微服务架构进行拆分。至少会包含以下服务用户服务负责注册、登录、鉴权。查询服务专门处理余票查询、车次查询等读请求。该服务重度依赖缓存其代码逻辑就是高效地组装缓存数据并处理缓存未命中时回源到数据库的流程。订单服务负责下单、占座的核心流程。它消费消息队列里的下单请求与库存服务交互管理订单状态机待支付、已支付、出票中、已完成等。库存服务最核心的服务管理席位库存的强一致性数据。提供原子性的库存查询和扣减接口。其背后是分库分表后的数据库集群。支付服务与各大银行、第三方支付渠道对接。消息推送服务负责将订单状态变更、余票信息等实时推送给用户APP或网页。数据层缓存集群以Redis为主采用Cluster模式实现高可用和分片扩展。用于缓存会话、查询结果、配置信息等。消息队列Kafka或RocketMQ。用于解耦下单流程实现流量削峰和异步处理。核心数据库MySQL集群采用分库分表如使用ShardingSphere等中间件。主库负责写和强一致性读多个从库负责分担应用层的读压力。大数据平台Hadoop/Hive/Spark用于离线分析历史订单、用户行为为票价动态调整、运力调度提供数据支持。4.2 关键中间件与技术选型考量在组件选型上每一层都有其考量负载均衡与网关Nginx性能优异生态成熟OpenResty在其基础上增加了LuaJIT可以实现更灵活的网关逻辑是构建高性能接入层的首选。在微服务内部会使用Spring Cloud Gateway或类似的网关进行路由、鉴权和限流。微服务框架Java生态中Spring Cloud Alibaba是一套常见组合Nacos注册中心、Sentinel流控、Seata分布式事务。选择它是因为阿里在双十一场景下验证过其可靠性且与RocketMQ等组件集成性好。当然Dubbo在纯RPC性能上可能更优但Spring Cloud的生态更完整。缓存与消息队列Redis几乎是内存缓存的事实标准。消息队列选择RocketMQ而非Kafka一个重要原因是RocketMQ提供了更好的事务消息支持这对于“下单扣库存”和“更新订单状态”这两个需要保证一致性的操作至关重要。RocketMQ的事务消息机制可以确保本地事务如扣库存和消息发送的最终一致性。数据库MySQL因其稳定性、生态和工具链的成熟依然是核心交易数据库的首选。分库分表是必须的需要结合业务设计良好的分片键。对于余票查询这种复杂查询可能还需要引入Elasticsearch作为查询引擎专门应对海量、复杂的搜索场景。5. 典型问题排查与性能优化实战5.1 线上高频问题与根因分析在实际运行中即使架构设计再完善也会遇到各种问题。以下是一些典型场景问题一用户反馈“看到有票一点提交就没了”。现象查询页面显示有余票点击提交订单后系统提示“占座失败席位已售完”。根因分析这是典型的缓存不一致或并发冲突问题。缓存延迟用户查询时命中的是尚未更新的缓存数据缓存未及时失效。真实库存已在另一笔交易中被扣减。乐观锁冲突在用户点击查询和点击提交的极短时间间隔内该席位被其他用户成功下单。当该用户的请求进入扣减流程时因版本号不匹配而失败。解决方案优化缓存失效策略确保库存变更后相关缓存能在毫秒级内失效。可以利用消息队列广播库存变更事件。在前端进行心理预期管理例如在查询结果旁提示“数据仅供参考以下单时为准”或在提交按钮处增加“排队中”状态降低用户预期。采用“预占”机制即用户点击查询后如果票量紧张系统可以为其预先锁定一个席位几秒钟类似购物车用户在这段时间内提交订单成功率会大增但这会消耗更多系统资源。问题二高峰期系统响应变慢甚至出现超时。现象页面加载慢查询接口超时下单排队时间极长。根因分析通常是链路中的某个环节达到瓶颈。数据库慢查询某些未走索引的复杂查询在高压下拖慢整个数据库。缓存击穿/雪崩大量请求同时查询一个不存在于缓存中的热点数据如某个突然热门的车次导致请求全部涌向数据库将其压垮。服务间调用链路过长或超时设置不合理一个用户请求可能调用多个微服务其中一个服务响应慢会拖累整个调用链。解决方案数据库加强SQL审核和慢查询监控对核心查询路径必须使用索引。对于分页查询等做好深度分页优化。缓存使用互斥锁Mutex Lock防止缓存击穿。即当缓存失效时不是所有线程都去查数据库而是让一个线程去查其他线程等待。对于缓存雪崩给不同的缓存数据设置随机的过期时间。服务治理实施严格的熔断、降级和限流策略。例如当查询服务调用库存服务超时应立即熔断并返回降级后的数据如默认无票或提示“系统繁忙”。对非核心功能如座位图展示、用户评价直接降级。5.2 全链路压测与容量规划对于12306这样的系统绝不能等到春运才看系统能承受多少压力。全链路压测是必须的。这意味着要在生产环境的隔离层如影子库、压测数据标记或完全复刻的压测环境中模拟真实用户从登录、查询到下单、支付的完整行为制造出与春运同量级甚至更高的流量。压测的目标是发现瓶颈找到在高压下最先出现性能问题的服务、数据库或中间件。验证预案验证熔断、降级、限流、弹性扩容等预案是否有效。容量评估确定当前系统架构的极限容量并以此为依据进行扩容规划。例如通过压测发现当前订单服务集群在每秒处理10万笔下单请求时CPU达到警戒线那么就需要提前扩容到能处理15万/秒的水平。容量规划需要结合业务数据进行。例如分析历史春运数据预测今年峰值时刻的并发用户数、查询QPS、下单TPS。然后根据压测得出的单机/单服务处理能力计算出需要多少台服务器。这中间还要考虑冗余通常预留30%-50%的余量和高可用部署多机房、异地多活。踩坑实录在一次模拟压测中我们发现下单TPS上不去。排查后发现瓶颈不在应用服务器也不在数据库而在消息队列的消费者。订单服务处理消息的速度跟不上生产者接入层的速度导致消息堆积。原因是消费者服务中有一段同步调用外部风控系统的逻辑耗时较长。解决方案是将同步调用改为异步先快速消费消息完成核心占座逻辑再将风控检查作为后续异步任务处理。这个案例说明全链路压测必须覆盖所有环节包括外部依赖。