一文读懂:高并发场景避免超卖少卖的实战攻略

嘿,兄弟们好,我是飞哥,临近过年没事,再来唠唠我做过的票务系统。

在票务这行,库存就是命脉。“超卖”(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. 别让“占座不买票”拖垮你:延时回滚策略

超卖防住了,那“少卖”怎么办?票务系统最怕用户抢了票不付钱。

我们的标准打法是:“预扣库存 + 延迟检查”。请看这张流程图:

飞哥敲黑板: 回滚库存时一定要注意幂等性。别因为网络抖动回滚了两次,那库存就凭空变多了,成了“灵异事件”。

5. 飞哥的血泪复盘:缓存和 DB 的“信任游戏”

记得刚入行那会儿,我有次只做了 Redis 扣减,没做后台对账。结果 Redis 意外宕机,重启后虽然有持久化,但还是丢了几个计数。

那天晚上,DB 里的订单票数和 Redis 里的库存数对不上,差了十几张。别小看这十几张票,那是几十通投诉电话和客服小姐姐的眼泪。

反思:缓存只是冲锋队的盾牌,数据库才是最后的防线。 现在我们的系统都会跑一个异步对账程序,每隔几分钟对一次账。如果发现 Redis 里的数和 DB 差异过大,立马报警并人工介入。

最后

很多人觉得搞定高并发就是堆机器、用牛逼的中间件。

其实干了这么多年票务,飞哥最深的感触是:技术方案没有完美的,只有最合适的。 你能防住 99.99% 的异常,剩下的 0.01% 靠的是完善的监控和快速响应的“ plan B ”。

写代码时多想一步“要是挂了怎么办”,你的系统就能比别人稳一倍。

更新好文,可关注《码上实战》

#面试##面试时最害怕被问到的问题#
全部评论

相关推荐

评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务