semaphore.acquire()成功后必须立即校验库存,不可直接扣减;校验失败或异常时需在finally中release();应使用公平模式并设为全局分布式信号量。

acquire() 后立刻校验库存,而不是直接扣减
很多人误以为 semaphore.acquire() 成功就等于“抢到了商品”,结果在数据库里批量写入负库存。Semaphore 只是计数器,不感知业务状态,它只管“有没有名额”,不管“名额对应的东西还在不在”。
- 正确做法:拿到许可后,必须立刻查 Redis 缓存(用 Lua 脚本保证原子性)或 DB 行锁(
SELECT ... FOR UPDATE),确认实时库存 ≥ 1 - 错误做法:
acquire()→ 直接UPDATE stock SET count = count - 1,多个线程同时读到旧值 100,最终写成 99、98……甚至 -23 - 校验失败必须
release(),否则信号量永久卡死——这是生产环境最常导致接口雪崩的点
release() 必须放在 try-finally 里,且不能只在 success 分支调用
一旦秒杀逻辑抛出异常(比如网络超时、Redis 连接断开、DB 主键冲突),而 release() 又没兜底执行,信号量许可就再也回不来了。
- 典型错误:
acquire()→ 业务代码 →if (success) release(),异常时跳过release() - 正确写法:用
try { ... } finally { semaphore.release(); }包裹整个临界区,哪怕校验失败、写库失败、甚至 JVM OOM,只要acquire()成功了,就必须归还 - 注意:不要在
catch里重复release(),finally已覆盖所有退出路径
公平模式(true)比非公平模式更适合秒杀准入阶段
秒杀开始瞬间涌进上万请求,非公平 Semaphore(100, false) 容易让少数线程反复抢占,其余大量请求排队超时;而公平模式强制 FIFO,能更均匀地分发“入场券”。
- 非公平模式:适合低延迟、短临界区场景(如日志写入),但秒杀中会放大响应时间毛刺
- 公平模式:
new Semaphore(100, true),虽然获取许可有轻微开销,但能避免饥饿,保障用户请求不被“插队”饿死 - 许可数别设成库存总数——应设为「预估峰值 QPS × 平均处理耗时」,例如 5000 QPS × 200ms = 1000,并发压测后微调
集群部署下,单机 Semaphore 完全失效
如果每个服务实例都 new 一个 Semaphore(100),那 10 台机器就相当于放行 1000 个并发,和库存 100 毫无关系。信号量必须是全局共享的。
- 单机限流只能防自己崩,防不了超卖;真正要控总量,得用 Redis 分布式信号量(如 Redis 的
INCR/DECR+ Lua)或集中式许可中心 - 若坚持用本地
Semaphore,仅可作为二级保护:先过全局 Redis 限流(第一道阀),再用本地信号量缓冲瞬时毛刺(第二道阀) - 千万别在 Spring Bean 里每次 new Semaphore——静态 final 才能复用,否则每请求新建对象,计数彻底乱套
实际落地时,最难的不是写对 acquire() 和 release(),而是把“准入”“校验”“提交”三步拆清楚,并确保每一步的失败都能正确回退。信号量只是漏斗口,漏斗下面还得接得住、判得准、写得稳。











