Java 分布式锁误用导致库存超卖的生产事故复盘:从错用到端到端治理
技术主题:Java 编程语言
内容方向:生产环境事故的解决过程(故障现象、根因分析、解决方案、预防措施)
引言
一次促销活动期间,我们的下单服务在高并发场景出现“偶发超卖”。最初怀疑是数据库隔离级别问题,深入排查后发现是分布式锁错误使用:锁未有效覆盖临界区、续期缺失导致锁误释放、以及针对跨服务写入缺少防重放的“围栏令牌”(Fencing Token)。本文复盘事故全过程,给出可直接落地的修复方案与代码骨架。
一、故障现象与影响
- 现象:
- 秒杀窗口内,库存数出现负值;
- 订单表与库存表不一致,重放补偿任务异常增多;
- 日志显示偶发“锁获取失败后仍继续下单”与“锁提前过期”。
- 影响:
- 业务侧投诉;
- 事后对账成本高,风控与补偿系统压力上升。
二、排查步骤
- 指标对齐:对比下单 QPS、锁获取失败率、平均持锁时长、数据库行锁等待;
- 采样日志:抽取同一商品在 1s 内的多实例日志,定位冲突批次;
- 代码走查:核对加锁与临界区边界是否一致,是否存在“加锁后调用远端再回写”的跨边界;
- Redis 观测:采样 key TTL 与持锁实例 ID,是否存在 watchdog 未续期;
- DB 版本:检查库存更新是否有乐观锁/版本号,是否能抵挡重放。
三、根因分析
- 锁与临界区不一致:加锁仅包裹“扣减库存”前的一小段逻辑,实际写入分散在多个 RPC 之后;
- 固定过期时间:使用固定 leaseTime(如 3s),遇到 JVM 停顿/下游抖动导致持锁超时被误释放;
- 无围栏令牌:下游写入没有“单调递增的令牌校验”,被误释放的旧持锁者仍可能覆盖新写入;
- 锁失败策略不当:tryLock 失败后未快速失败,而是继续业务流程。
四、修复方案(架构与工程双管齐下)
- 锁的“边界与归属”
- 临界区要与“对库存状态产生最终写入”的边界一致;
- 优先把扣减库存收敛为一个原子操作(本地事务或单服务内完成)。
- 续期与超时
- 使用 Redisson 的 watchdog 自动续期(不设置固定 leaseTime),或按最长 P99 处理时长评估合理超时;
- 围栏令牌(Fencing Token)
- 在获取锁的同时生成单调递增的 token,所有写路径携带 token,由下游以“令牌更大者为准”进行校验;
- 双重防线
- 应用层锁 + 数据层乐观锁(version/updated_at)共同保障;
- 失败即快返
- tryLock 获取不到立即失败,避免排队放大延迟;
- 观测与压测
- 采集持锁时长、续期次数、锁竞争度、库存版本冲突率;在压测环境覆盖 GC 停顿与下游抖动场景。
五、关键代码(Redisson + Fencing Token)
5.1 正确的 Redisson 加锁用法
1 | // Maven 依赖:org.redisson:redisson |
说明:
- tryLock 等待时间很短(如 200ms),拿不到就让上游退避/排队限流;
- 不设置固定 leaseTime,交给 watchdog;若你的处理存在长阻塞,仍需压测确认续期是否可靠;
- 临界区尽可能短,并在本地完成“读-判-写”。
5.2 Fencing Token 与乐观锁示例
1 | // 令牌服务:可用 Redis INCR 或数据库序列,保证单调递增 |
说明:
- Fencing Token 防止“旧锁持有者”在锁被误释放后继续写入;
- 乐观锁保证并发写入的线性化,避免并发覆盖;
- 在跨服务写入(例如库存服务 + 订单服务)时,所有写路径都需要携带并校验 token。
5.3 观测与告警建议
- Redisson 指标:持锁时长、续期次数、锁竞争失败率;
- 业务指标:每 SKU 的版本冲突率、令牌拒绝率、库存负值防线触发次数;
- 告警阈值:
- 续期异常比率 > 0.5%(1 分钟窗口)
- tryLock 超时率 > 5% 且下单 QPS 上升
- 版本冲突率 P95 > 2%
六、效果与验证
- 压测场景:
- 注入 100-300ms 抖动与 1-2s 尖刺;
- 模拟 GC 停顿(手工触发或压测机制造压力);
- 异常断电/进程崩溃,验证锁与 token 的一致性约束仍生效。
- 结果(示意):
- 促销峰值超卖降至 0;
- tryLock 失败率 < 3%,平均持锁时长 P95 < 40ms;
- 版本冲突率 < 1%,无库存负值。
七、防复发清单
- 设计层面:
- 确认临界区边界;
- 评估是否可用单机原子操作(如 DB 原子扣减)替代分布式锁。
- 工程层面:
- Redisson 统一封装:tryLock 短等待、watchdog、finally 解锁;
- 引入 Fencing Token 与版本号,在所有写路径校验;
- 失败快返 + 上游退避限流;
- 完整可观测性:锁指标、版本/令牌指标、数据库冲突;
- 压测用例纳入回归(GC/网络抖动/下游慢)。
总结
这次事故的关键不在“有没有加锁”,而在“锁是否覆盖正确的临界区、能否在异常条件下维持顺序”。通过 Redisson 的正确使用(短等待+续期+finally 解锁)、围栏令牌与乐观锁双重校验、以及全面的观测与压测,我们将库存链路从“偶发失序”拉回到“可验证的线性化”。以上代码骨架可直接迁移并作为团队基线,在此之上再做容量与性能优化。