引言
在上一篇文章《重新理解12306:它卖的从来不是“库存”,而是“状态”》,我们深入探讨了12306的业务模型核心:它不是简单的库存管理系统,而是基于座位段的状态管理。每个座位被拆分为多个段(例如A-B、B-C、C-D等),售出票时实际是原子性地锁定连续段的使用权。这种模型带来了独特的并发挑战——如何在高并发请求下快速拒绝无效订单,减轻下游系统压力。
假设一列车有1000个座位,同一时刻可能有100万(瞎猜)用户抢票,但最终能成功的只有极少数。如果让所有请求都进入核心下单流程,系统必然崩溃。那么,如何在网关层就识别并拒绝绝大部分无效请求?传统电商的库存计数方案在这里失效了,因为12306的“库存”是动态关联的座位段。
本文将揭示一种高效的架构方案:基于区间计数器的网关层拒单策略。通过这种方案,我们能够将百万并发过滤为可管理的十几万请求,保证系统稳定运行。
核心思路:区间计数器粗粒度过滤
为什么传统库存计数方案不适用?
在传统电商中,我们可以在网关层设置库存计数器:当某商品库存为0时,直接拒绝所有购买请求。但12306的情况截然不同:
- 它不是简单减库存,而是需要找到连续座位段并锁定
- 同一个座位可以被拆分为多个区段(如A-B、B-C、C-D)售予不同乘客
- 不同区间组合(如北京-上海、北京-南京、南京-上海)相互关联且竞争同一座位资源
区间计数器的基本原理
虽然12306的座位段模型复杂,但有一个关键特性:对于任意区间组合(如北京南-上海虹桥),最多可售出的票数不会超过列车总座位数。也就是说,无论有多少种不同的上下车组合,一趟列车的总运力是固定的。
基于这一原理,我们可以为每个区间组合设置一个计数器:
- 当某个区间组合的请求数达到列车总座位数时,后续请求可直接拒绝
- 例如:一列车有1000个座位,那么"北京南-上海虹桥"这个区间最多只能有1000个请求通过
计数器数量计算
假设一趟列车有20个站点,那么可能的区间组合数量为:
C(20, 2) = 20 × 19 / 2 = 190
如果列车有1000个座位,那么网关层需要处理的最大请求量为:
190 × 1000 = 171,000
这个数字相比最初的100万并发,已经减少了83%以上!而且这171000请求还可以通过缓存和异步处理进一步消化。
架构设计
整体架构图
用户请求 → 网关层 → Redis计数器 → 消息队列 → 后端服务 → 数据库
详细流程
1. 用户请求到达网关
- 请求包含车次、起始站、终点站信息
- 示例:{车次: "G101", 起始站: "北京南", 终点站: "上海虹桥"}
2. 生成区间键
- 根据请求参数生成唯一键
- 示例:生成键 "G101:北京南:上海虹桥"
3. 计数器检查
- Redis计数器值 ≥ 总座位数(如1000),立即拒绝请求,返回“无票”
- 否则,原子性地递增计数器,并允许请求通过
4. 异步处理
- 通过的请求被送入消息队列(如Kafka或RabbitMQ)
- 后端服务异步消费队列,执行实际的座位段锁定和订单创建
5. 计数器校准(可选)
- 后端处理完成后,根据结果调整计数器
- 处理失败时递减计数器,避免过度拒绝
关键组件详解
网关层
网关层是整个系统的第一道防线,需要具备极高的性能和可靠性:
- 选择高性能API网关:如Nginx+OpenResty
- 实现限流机制:防止单个用户或IP发送过多请求
- 保持无状态设计:方便水平扩展
Redis计数器存储
Redis作为内存数据库,提供高速的计数器操作:
- 使用原子操作:保证并发下的数据一致性
- 设置过期时间:自动清理过期车次的计数器
- 分片存储:根据车次或区间哈希分布到多个实例,避免热点
消息队列
消息队列起到削峰填谷的作用:
- 选择高吞吐量消息队列:如Kafka或RabbitMQ
- 配置多个消费者组:并行处理不同车次的请求
- 实现死信队列:处理多次失败的消息
后端服务
后端服务负责精确的座位分配:
- 实现分布式事务:保证座位锁定和订单创建的一致性
- 设计幂等操作:防止重复处理同一请求
- 采用批量处理:提高数据库操作效率
方案优势
- 高效过滤:能在网关层拒绝80%以上的无效请求
- 简单可靠:基于Redis的计数器方案易于实现和维护
- 可扩展性:通过分片和异步处理,系统可以水平扩展
总结
12306这类系统的高并发挑战并非无解。关键在于识别出业务模型中“每区间出票存在上限”这一核心特征,并据此在网关层实施区间计数过滤。通过粗粒度的请求控制,将极大部分的无效并发挡在门外,从而为后端复杂事务争取更多的处理资源与时间。