
做票务系统的同学大概都遇到过这个需求:"我们景区要上分时预约,加个预约功能就行。"
如果真按"加个功能"来做,上线后大概率会收到三类工单:某时段显示约满但现场没满;同一身份证约到了两个时段;退票之后名额没有回到可约池。
这不是业务没想清楚,而是库存模型选错了。
直接答案
分时预约的库存不是标量,而是一个三维矩阵:时段 × 票种 × 身份占用状态。把矩阵当标量("还剩 N 张")来建模,必然出现超卖与假满。工程上的正解是"分时段独立库存 + 预占/确认两阶段 + 取消即回池"。
一、为什么标量库存会失败
传统售票系统的库存模型很朴素:stock 是一个整数,下单减一,退票加一。
这个模型在"一次入园"场景下没问题,但在分时预约下会同时踩三个坑:
- 跨时段超卖 :
stock是全天的总数,10 点的名额被约光后,系统仍允许约 11 点,因为总数还有余量。 - 并发超卖 :
stock--是非原子操作时,高并发下会击穿。 - 假满:取消订单只加回总数,没有加回对应时段的余量,于是"总量还有,时段约不到"。
根因:分时预约的约束不是"总量",而是"每个时段的余量",而后者是一个带维度的量。
二、正解:三维库存 + 两阶段提交
2.1 库存维度
css
stock[time_slot][ticket_type] = 剩余可约数
每个时段独立库存,独立校验。跨时段不共享余量。
2.2 两阶段:预占(reserve)与确认(commit)
下单不直接扣库存,而是先"预占":
reserve:在目标时段预占 1 个名额,写一条带 TTL 的预占记录(例如 15 分钟)。commit:支付成功后转正,预占记录变为已占用。release:超时未支付或用户取消,预占记录过期自动释放,余量秒级回池。
这一步是"假满"的解药。没有 TTL 预占的系统,订单和库存是两套账,迟早对不上。
2.3 并发控制
- 余量扣减用原子操作(Redis
DECR/ 数据库UPDATE ... WHERE stock > 0)。 - 强一致场景用分布式锁或队列串行化,把"同一时段"的扣减收敛到单点。
三、核销与状态的闭环
预约系统里,订单状态比库存更容易出错。建议的状态机:
scss
预占 → 已支付(未核销) → 已核销
↘ 已取消(退款) → 库存回池
↘ 已过期(未核销未取消) → 库存回池
"已过期"这一支最容易被漏掉。 未核销又没取消的订单,如果一直占着名额,就是"死库存"。
核销端要满足两个工程要求:
- 离线核销:景区门口网络最差,核销端要能本地缓存名单,断网时先验后同步。
- 幂等核销:同一张票重复扫码不能重复计数,用核销流水号去重。
四、渠道与风控:让抢到的票无法交付
技术代抢的本质是"解析接口 + 批量占位"。工程上的应对不是封 IP,而是提高"变现成本":
- 订单与证件强绑定:抢到的票无法转让,倒卖即失效。
- 设备指纹 + 行为频次:异常账号进入风控队列,人工复核。
- 预占限流:单账号未支付预占数量上限,避免"占坑"。
中新网 2026 年 8 月报道过一起案件:有人利用软件抢票一年半、抢下两万余张、非法获利近 600 万元,最终以非法获取计算机信息系统数据罪获刑。技术代抢的法律风险是真实的,但更有效的防线是让票无法交付。

五、验收清单(工程视角)
| # | 验收项 | 工程判据 |
|---|---|---|
| 1 | 库存维度 | stock[时段][票种] 独立,不共享 |
| 2 | 预占机制 | 有 TTL,过期自动释放 |
| 3 | 并发安全 | 原子扣减或串行化 |
| 4 | 状态机 | 含"已过期"分支,死库存可回收 |
| 5 | 核销 | 支持离线 + 幂等 |
| 6 | 退改 | 退款与库存回池同一事务 |
| 7 | 风控 | 订单证件强绑定 + 频次限制 |
| 8 | 数据导出 | 全量订单/核销可导出 |
六、FAQ
Q:用消息队列削峰就够了吗? 不够。削峰解决的是流量,不解决库存维度错误。维度错了,削峰只是把错误推迟。
Q:预占 TTL 设多长合适? 通常 10-15 分钟,结合支付渠道的回调时效。太短会误释放,太长会压库存。
Q:历史数据要不要迁移? 要。系统锁没锁死,不看价格,只看一句话:三年后想换供应商,这些数据能不能完整带走。
库存不是一个数,而是一个带时间的矩阵。 核销端能不能离线验票,才是票务系统的成人礼。 模型错了,限流只是把排队搬到了线上。
写在最后
分时预约不是售票系统的一个功能开关,而是一次库存模型的替换:从"还剩多少张"到"哪个时段还剩多少、被谁占着、什么状态"。
模型对了,限流才是限流;模型错了,限流只是把排队搬到了线上。