Redis分布式锁别瞎用,我的数据差点被覆盖

发布时间:2026/10/5 13:33:11
Redis分布式锁别瞎用,我的数据差点被覆盖 上周三凌晨我盯着监控面板上的订单数据心跳加速——10分钟内的库存扣减记录里同一个SKU居然出现了6次重复扣减。事后排查发现正是团队里那个看似靠谱的Redis分布式锁实现在流量突增时放行了并发请求。今天我们就来解剖这个差点酿成大祸的伪锁。一、现象锁了但没完全锁当时我们的秒杀系统正在做全链路压测QPS刚突破3k时监控突然报警库存异常。查看Redis锁日志时发现了诡异现象[16:23:45] 客户端A 获取锁成功: order_1234 (TTL30s) [16:23:46] 客户端B 获取锁成功: order_1234 (TTL30s) -- WTF?两个不同的客户端在1秒内先后拿到了同一个订单ID的锁。最终导致库存服务处理了完全相同的6个请求商品超卖不说财务对账时发现账户余额也出现了负数。二、根因自以为是的原子性翻出当时祖传的锁实现代码问题一目了然// 错误示范三宗罪齐备的锁实现 public boolean tryLock(String key) { Long expireTime System.currentTimeMillis() 30000; if (redisTemplate.opsForValue().setIfAbsent(key, expireTime)) { // 罪1非原子操作 redisTemplate.expire(key, 30, TimeUnit.SECONDS); // 罪2非原子续期 return true; } Long oldExpireTime (Long) redisTemplate.opsForValue().get(key); if (oldExpireTime ! null oldExpireTime System.currentTimeMillis()) { redisTemplate.opsForValue().set(key, expireTime); // 罪3竞态条件 return true; } return false; }这段代码犯了三个致命错误SETNXEXPIRE非原子两个命令之间如果进程崩溃会导致死锁时间校验不可靠客户端时钟不同步时可能误判锁过期删除校验缺失解锁时不做value比对可能误删其他客户端持有的锁三、救火从Redlock到红皮书方案当时紧急回滚后我们测试了三种解决方案方案吞吐量(QPS)强一致性复杂度原生SET NX PX12k❌⭐Redlock5k✅⭐⭐⭐红皮书续期线程8k✅⭐⭐最终选择了《Redis设计与实现》推荐的改进方案// 正确姿势Lua脚本保证原子性 String script if redis.call(setnx, KEYS[1], ARGV[1]) 1 then redis.call(pexpire, KEYS[1], ARGV[2]) return 1 else return 0 end; Boolean success redisTemplate.execute( new DefaultRedisScript(script, Boolean.class), Collections.singletonList(lockKey), clientId, // 必须用唯一值 30000 );关键改进点原子操作用Lua脚本打包SETNX和EXPIRE唯一标识value使用clientId线程ID避免误删续期机制另起线程对未完成的锁定期续期四、避坑指南血泪换来的经验时钟跳跃是大敌NTP同步时可能导致锁提前过期优先使用TTL而非时间戳网络延迟会骗人业务处理时间超过锁有效期时CAP理论会教你做人单点故障无解Redis主从切换时的锁丢失问题Redlock也救不了锁重入要谨慎相同线程重复获取锁时Java的ReentrantLock思维会害了你五、什么时候该用分布式锁经过这次事故我们定下新规范读多写少场景 → 直接用CAS低频写操作 → 数据库乐观锁高频写且允许少量偏差 → 本地锁延时队列必须强一致时→ Redis锁Zookeeper备份现在看监控面板上的锁争用指标时总会多瞄两眼——你在项目里是怎么处理分布式锁的有没有遇到过更诡异的锁失效案例评论区聊聊你的实战经历。