出行节奏越来越快
12306作为中国铁路官方购票平台
每天处理着数以亿计的购票请求
但你了解吗
他背后的技术架构远比我们想象的要复杂得多

我也曾参与演唱会票务系统、抢购功能的建设
也会思考国内顶流12306解决方案时怎样实现的
今天,我们探讨12306两个常见但复杂的购票需求:
这两种票务问题
传统的电商系统
最小单位是商品 SKU
每个商品有库存
售完即止
而火车票的商品是弹性的
例如 北京 - 广州 京广线

一个座位可以是一张全程票
也可以是多张半程票
比如途经 17个站点
则每个座位会有 C(17, 2) 个组合,即136种商品
座位数就是库存量
所以会带来新的问题
某个区段最多允许出多少张票
某个区段最少允许出多少张票
某个站点上车的最多多少张
我们从最基础的情形开始
如何在保证座位物理连续性的同时
高效地分配座位
12306采用了座位池管理机制
将车厢内的座位按照物理位置进行分组
每个座位池代表车厢内的一段连续座位
1车厢1排 A、B、C 号座位
1车厢1排 D、F 号座位
当用户选择"连续座位"时
系统会查询可用的座位池
确保座位在物理上是连续的
系统在车次开售前
根据车厢布局和座位配置
预先计算出所有可能的连续座位组合
并标记为"可用"或"已售"
当用户请求连续座位时
如选购 2 张连续的二等座
系统首先根据用户指定的座位类型
过滤可用座位池
然后根据座位连续性要求筛选出符合条件的座位池
这种机制确保了用户能够成功购买到连续座位
同时避免了因座位分配不当导致的系统混乱
长途旅程带老人和小孩时
卧铺购买会更倾向选择下铺
当用户要求 1 个下铺 + 1个中铺 时
12306需要处理不同座位类型的组合购买
这涉及到更复杂的配额管理和座位分配逻辑
首先在定义了不同的座位类型
例如 上铺、中铺、下铺
为每个车次、每个区段设置座位类型配额
例如北京到石家庄
下铺配额为10张,中铺配额为20张
当购票请求时
会根据座位类型和用户需求
优先分配用户指定的座位类型
当某个座位类型配额即将用完时
系统会自动调整售票策略

在处理连续座位请求时
采用了乐观锁机制
系统首先查询可用的座位池
然后尝试在数据库中标记这些座位为"已预留"
如果更新成功
则返回座位信息
失败则重试或提示用户选择其他座位
public class SeatAllocator {
// 座位池缓存:key = [车次+区段+座位类型], value = 可用座位池列表
private final Map<String, List<SeatPool>> seatPoolCache = new ConcurrentHashMap<>();
// 配额管理:key = [车次+区段+座位类型], value = 当前剩余配额
private final Map<String, Integer> quotaManager = new ConcurrentHashMap<>();
// 预留订单缓存:key = 订单ID, value = 预留座位列表 + 超时时间戳
private final Map<String, Reservation> reservationCache = new ConcurrentHashMap<>();
// 1. 初始化座位池(系统启动时预加载)
public void initSeatPools(String trainId, String segment, String seatType, int totalSeats) {
// 实际实现:从数据库加载车厢布局,生成连续座位池
List<SeatPool> pools = generateContinuousPools(segment, seatType, totalSeats);
seatPoolCache.put(trainId + "_" + segment + "_" + seatType, pools);
quotaManager.put(trainId + "_" + segment + "_" + seatType, totalSeats);
}
// 2. 核心分配方法(处理连续/混合场景)
public boolean allocateSeats(BookingRequest request) {
if (request.isContinuous()) {
return allocateContinuous(request);
} elseif (request.isMixedType()) {
return allocateMixed(request);
} else {
return allocateStandard(request);
}
}
// 3. 连续座位分配逻辑
private boolean allocateContinuous(BookingRequest request) {
String key = request.getTrainId() + "_" + request.getSegment() + "_" + request.getSeatType();
// 1. 获取可用座位池(按物理连续性筛选)
List<SeatPool> pools = seatPoolCache.getOrDefault(key, Collections.emptyList());
SeatPool selectedPool = findContinuousPool(pools, request.getSeatCount());
if (selectedPool == null) {
returnfalse; // 无连续座位
}
// 2. 配额原子性检查(防止超卖)
if (!checkQuotaAndReserve(key, request.getSeatCount())) {
returnfalse;
}
// 3. 分配座位(标记为预留)
List<Seat> allocatedSeats = selectedPool.allocateSeats(request.getSeatCount());
reservationCache.put(request.getOrderId(), new Reservation(allocatedSeats, System.currentTimeMillis() + 45 * 60000));
returntrue;
}
// 4. 混合铺位分配逻辑(如卧铺+中铺)
private boolean allocateMixed(BookingRequest request) {
List<String> seatTypes = request.getSeatTypes(); // [卧铺, 中铺]
List<Integer> counts = request.getSeatCounts(); // [1, 1]
// 1. 逐类型检查配额并预留
for (int i = 0; i < seatTypes.size(); i++) {
String type = seatTypes.get(i);
String key = request.getTrainId() + "_" + request.getSegment() + "_" + type;
if (!checkQuotaAndReserve(key, counts.get(i))) {
returnfalse; // 任一类型配额不足
}
}
// 2. 为每种类型分配座位(独立池)
List<Seat> allSeats = new ArrayList<>();
for (int i = 0; i < seatTypes.size(); i++) {
String type = seatTypes.get(i);
String key = request.getTrainId() + "_" + request.getSegment() + "_" + type;
SeatPool pool = findAnyPool(seatPoolCache.getOrDefault(key, Collections.emptyList()));
allSeats.addAll(pool.allocateSeats(counts.get(i)));
}
// 3. 保存混合分配结果
reservationCache.put(request.getOrderId(), new Reservation(allSeats, System.currentTimeMillis() + 45 * 60000));
returntrue;
}
// 购票请求格式
class BookingRequest {
private String trainId;
private String segment;
private String seatType;
private List<String> seatTypes; // 混合铺位时使用
private List<Integer> seatCounts; // 每种类型的数量
private int seatCount;
private boolean isContinuous;
private boolean isMixedType;
private String orderId;
}
}
以上是伪代码片段
在真实场景下
还有高并发、实时更新、抢票行为等
为用户提供高效、公平的购票体验
其核心的领域模型设计和座位管理技术
都将是支撑12306持续为数亿用户提供可靠服务的关键