在票务这行库存就是命脉。“超卖”Over-selling让你赔钱丢名声“少卖”Under-selling让老板觉得你技术不行票明明有却卖不出去。今天飞哥就结合这几年在票务系统摸爬滚打的经验跟大家好好唠唠这里面的深水区。1. 为什么“超卖”和“少卖”是系统的生死劫很多兄弟初学并发觉得写个synchronized或是ReentrantLock就能高枕无忧了。但在分布式架构下这就像是用塑料袋去兜洪水。超卖就像 10 个人同时挤进一个窄门大家看到货架上还有最后一张票结果 10 个人都下单成功了。少卖又叫“库存空转”。用户抢了票占了座结果不付钱。你把票锁死了别人买不到最后演出开始了座位还空着白白浪费钱。2. 三个段位的防御战从行锁到 Lua 脚本咱们票务系统处理库存通常会经历三个阶段。我做了个对比表大家对号入座方案技术手段优点缺点适用场景青铜DB 行锁 (UPDATE...WHERE stock 0)绝对一致简单粗暴并发一高数据库直接宕机内部员工购票、小场次白银分布式锁 (Redisson)逻辑清晰保护 DB锁竞争剧烈响应时间长中等流量促销黄金Redis Lua 脚本原子操作极高性能逻辑略复杂需考虑一致性大促、万人抢票首选3. 看家本领Redis Lua 丝滑扣减在抢票这种瞬时爆发场景我们通常把库存预热到 Redis 里。为什么一定要用 Lua因为 Redis 执行 Lua 脚本是原子性的。它能保证“查询库存 - 判断余量 - 扣减库存”这三步像德芙一样丝滑中间不会被任何请求插队。Java 核心逻辑参考// Lua 脚本原子扣减 String luaScript local stock tonumber(redis.call(get, KEYS[1])) if (stock 0) then redis.call(decr, KEYS[1]) return 1 // 扣减成功 else return 0 // 库存不足 end; // 执行扣减 Long result redisTemplate.execute( new DefaultRedisScript(luaScript, Long.class), Collections.singletonList(show_101_stock) ); if (result 1) { // 抢到预扣名额赶紧去异步创建订单 sendOrderMessage(userId, showId); } else { throw new BusinessException(票已售罄下次早点来); }4. 别让“占座不买票”拖垮你延时回滚策略超卖防住了那“少卖”怎么办票务系统最怕用户抢了票不付钱。我们的标准打法是“预扣库存 延迟检查”。请看这张流程图用户Redis库存延时队列数据库等待用户支付...alt[未支付][已支付]1. 抢票 (Lua 原子扣减)扣减成功2. 生成待支付订单3. 发送 15 分钟延迟消息4. 检查支付状态5. 回滚库存 (Incr)6. 取消订单7. 更新为支付成功用户Redis库存延时队列数据库敲黑板回滚库存时一定要注意幂等性。别因为网络抖动回滚了两次那库存就凭空变多了成了“灵异事件”。5. 飞哥的血泪复盘缓存和 DB 的“信任游戏”记得刚入行那会儿我有次只做了 Redis 扣减没做后台对账。结果 Redis 意外宕机重启后虽然有持久化但还是丢了几个计数。那天晚上DB 里的订单票数和 Redis 里的库存数对不上差了十几张。别小看这十几张票那是几十通投诉电话和客服小姐姐的眼泪。反思缓存只是冲锋队的盾牌数据库才是最后的防线。现在我们的系统都会跑一个异步对账程序每隔几分钟对一次账。如果发现 Redis 里的数和 DB 差异过大立马报警并人工介入。