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

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

在票务这行,库存就是命脉。 "超卖" (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 核心逻辑参考:

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. 别让"占座不买票"拖垮你:延时回滚策略

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

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

sequenceDiagram participant U as 用户 participant R as Redis库存 participant D as 延时队列 participant B as 数据库 U->>R: 1. 抢票 (Lua 原子扣减) R-->>U: 扣减成功 U->>B: 2. 生成待支付订单 B->>D: 3. 发送 15 分钟延迟消息 Note over D: 等待用户支付... D->>B: 4. 检查支付状态 alt 未支付 B->>R: 5. 回滚库存 (Incr) B->>B: 6. 取消订单 else 已支付 B->>B: 7. 更新为支付成功 end

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

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

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

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

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


最后

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

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

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

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

相关推荐
m0_607076606 小时前
CSS3 转换,快手前端面试经验,隔壁都馋哭了
前端·面试·css3
青云计划7 小时前
知光项目知文发布模块
java·后端·spring·mybatis
Victor3567 小时前
MongoDB(9)什么是MongoDB的副本集(Replica Set)?
后端
NEXT067 小时前
二叉搜索树(BST)
前端·数据结构·面试
Victor3567 小时前
MongoDB(8)什么是聚合(Aggregation)?
后端
NEXT067 小时前
JavaScript进阶:深度剖析函数柯里化及其在面试中的底层逻辑
前端·javascript·面试
yeyeye1118 小时前
Spring Cloud Data Flow 简介
后端·spring·spring cloud
Tony Bai9 小时前
告别 Flaky Tests:Go 官方拟引入 testing/nettest,重塑内存网络测试标准
开发语言·网络·后端·golang·php
牛奶9 小时前
《前端架构设计》:除了写代码,我们还得管点啥
前端·架构·设计
+VX:Fegn08959 小时前
计算机毕业设计|基于springboot + vue鲜花商城系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计