上周朋友吐槽,公司搞秒杀活动,用户点十几次‘立即抢’都没反应——后台日志里满屏的 LockConflictException。不是数据库挂了,也不是网络抖动,而是分布式锁在悄悄‘打架’。
锁没抢到,不等于没加锁
很多人以为:加锁失败 = 没锁住,程序直接走下一步。但现实是,某些锁实现(比如基于 Redis 的 SETNX + 过期时间)在超时释放前,旧锁还占着坑,新请求一来就撞上——这时候系统未必报错,但业务逻辑已悄然错乱。比如库存扣减被跳过、订单状态卡在‘待支付’、同一张优惠券被发给两个用户。
怎么看出锁正在冲突?
别只盯着返回值是否为 true。真正要盯的是三类信号:
- Redis 中锁 key 的 TTL 频繁重置(说明多个节点反复争抢)
- 应用日志中出现大量
Failed to acquire lock for resource: order_12345 - 监控图表里锁等待耗时突然拉长(比如 P99 从 5ms 跳到 800ms)
一个接地气的检测小技巧
在加锁代码后加一行埋点:
if (!lock.tryLock(3, 10, TimeUnit.SECONDS)) {
log.warn("[LOCK-CONFLICT] resource={}, waitMs={}", resourceId, System.currentTimeMillis() - startTime);
throw new BusinessException("操作太忙,请稍后再试");
}
上线后用 ELK 或 Grafana 把 LOCK-CONFLICT 日志单独聚合,就能看清哪些资源是‘锁战热点’——比如 coupon_888 每分钟冲突 200+ 次,那八成是前端没做按钮防抖,用户狂点造成的。
别让‘自动续命’变‘锁拖油瓶’
用 Redisson 的 RLock.lock() 自带看门狗续期很省心?小心!如果业务方法执行慢(比如调第三方接口卡住),看门狗会一直续,导致锁长期霸占。这时另一个服务实例想加锁,就会陷入长时间等待或直接超时。建议:关键路径上显式控制锁持有时间,比如用 lock.lock(5, TimeUnit.SECONDS) 强制最久只拿 5 秒,超时就重试或降级。
真实场景对照表
| 现象 | 可能原因 | 快速验证法 |
|---|---|---|
| 同一笔订单创建出两条记录 | 锁 key 设计未包含业务唯一标识(如用了固定 key order_lock) |
查 Redis:KEYS order_lock*,看是否只有一条匹配 |
| 后台任务每天凌晨卡住半小时 | 定时任务锁过期时间设成 1 小时,但某次执行耗时 72 分钟,锁提前释放,触发多实例并发 | 查任务日志起止时间 + 对应锁 key 的 TTL 值 |