分布式锁实战:防重下单、设备独占控制 分布式锁实战防重下单、设备独占控制同事写了个秒杀接口测试时手速过快连点了20下鼠标结果一个用户下了20单。他转头问我“哥咋防重” 我说“兄弟你需要一把锁——不是锁门的那种是锁并发的。”一、分布式锁的应用场景在单机时代synchronized或ReentrantLock就能搞定并发问题。但微服务是多实例部署的JVM 锁只能控制当前进程跨进程就管不到了。这时候需要一把能横跨所有服务实例的锁——分布式锁。典型场景场景说明不锁的后果防重下单用户快速点击多次扣钱多次只出一次货库存扣减并发减库存超卖库存变负数设备独占同一设备同一时间只能被一个用户操作A用户的操作被B用户覆盖定时任务防重多实例同时跑定时任务重复执行数据错乱二、四种分布式锁方案对比方案原理优点缺点DB 唯一索引INSERT 冲突 → 加锁失败实现简单性能差无过期释放Redis SETNXSET key value NX EX高性能易用锁过期业务没完Redisson基于Redis的高层封装功能最全依赖Redis高可用ZooKeeper临时顺序节点CP一致性极强性能不如Redis维护复杂推荐业务场景优先选 RedissonRedis 封装因为大多数系统本来就有 Redis开箱即用。除非对一致性要求极高如金融资金清算否则没必要上 ZooKeeper。三、原生 Redis 实现分布式锁// 加锁StringlockKeylock:order:userId;BooleanlockedredisTemplate.opsForValue().setIfAbsent(lockKey,uuid,30,TimeUnit.SECONDS);// NX EXif(!Boolean.TRUE.equals(locked)){thrownewBizException(操作太频繁请稍后再试);}// 释放锁 —— 必须用 Lua 脚本保证原子性Stringluaif redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end;redisTemplate.execute(newDefaultRedisScript(lua,Long.class),Collections.singletonList(lockKey),uuid);为什么释放锁要用 Lua 脚本如果分两步先 get 再 del中间可能发生 GC 停顿。Redis 判断 value 相等后准备删除结果下一秒锁已过期被别人抢走——你删的就是别人的锁了。Lua 脚本让判断删除变成一个原子操作杜绝此风险。上面这段代码还有一个致命问题如果业务逻辑执行了 40 秒锁在 30 秒时就自动过期了——别的人又拿到了锁导致并发问题。四、Redisson专业级的分布式锁Redisson 是 Redis 官方推荐的 Java 客户端它对分布式锁做了很厚的封装把上述问题全部解决。ConfigurationpublicclassRedissonConfig{BeanpublicRedissonClientredissonClient(){ConfigconfignewConfig();config.useSingleServer().setAddress(redis://127.0.0.1:6379);returnRedisson.create(config);}}4.1 看门狗Watchdog自动续期Redisson 最核心的机制。你设置锁过期 30 秒如果 20 秒后业务还在跑看门狗自动帮你续到 30 秒。业务完成后锁自动释放不会长期占用。RLocklockredissonClient.getLock(lock:order:userId);lock.lock();// 默认30秒过期看门狗每10秒续一次try{// 业务逻辑随便执行多久}finally{lock.unlock();// 看门狗停止续期}4.2 tryLock带超时的非阻塞加锁booleanlockedlock.tryLock(5,60,TimeUnit.SECONDS);// 等待5秒拿到后持有60秒if(!locked){thrownewBizException(系统繁忙请稍后再试);}4.3 可重入锁同一个线程多次lock.lock()不会阻塞自己——自动计数lock.lock();// 持有计数 1lock.lock();// 持有计数 2lock.unlock();// 持有计数 1lock.unlock();// 持有计数 0真正释放4.4 公平锁/读写锁// 公平锁按申请顺序排队获取RFairLockfairLockredissonClient.getFairLock(lock:order);// 读写锁读并发、写互斥RReadWriteLockrwLockredissonClient.getReadWriteLock(lock:device);rwLock.readLock().lock();// 多个读操作可以并发rwLock.writeLock().lock();// 写操作互斥读写锁非常适合设备状态读取频繁但修改低频的场景。五、实战1防重下单ServicepublicclassOrderService{AutowiredprivateRedissonClientredissonClient;TransactionalpublicOrdercreateOrder(LonguserId,ListOrderItemitems){StringlockKeylock:order:user:userId;RLocklockredissonClient.getLock(lockKey);booleanacquiredfalse;try{acquiredlock.tryLock(3,30,TimeUnit.SECONDS);if(!acquired){thrownewBizException(请勿重复提交订单);}// 幂等性检查30秒内同一用户的重复请求StringidempotentKeyorder:submit:userId:requestHash(items);BooleanexistsredisTemplate.opsForValue().setIfAbsent(idempotentKey,1,30,TimeUnit.SECONDS);if(!Boolean.TRUE.equals(exists)){thrownewBizException(订单已提交请勿重复操作);}// 创建订单...returnorder;}finally{if(acquiredlock.isHeldByCurrentThread()){lock.unlock();}}}}双层防护外层分布式锁防并发内层幂等键防重复提交。两者结合才是真正可靠的防重方案。六、实战2设备独占控制在物联网/无人售货柜场景中一台设备同一时间只能被一个操作员操作否则指令冲突导致状态混乱。ComponentpublicclassDeviceLockManager{AutowiredprivateRedissonClientredissonClient;privatefinalConcurrentHashMapString,RLocklockCachenewConcurrentHashMap();publicbooleanacquireDevice(LongdeviceId,LongoperatorId,inttimeoutSec){Stringkeylock:device:deviceId;RLocklockredissonClient.getLock(key);booleanacquiredlock.tryLock();if(acquired){lockCache.put(key,lock);// 记录谁持有了这把锁redisTemplate.opsForValue().set(lock:device:owner:deviceId,String.valueOf(operatorId),Duration.ofSeconds(timeoutSec));}returnacquired;}publicvoidreleaseDevice(LongdeviceId){Stringkeylock:device:deviceId;RLocklocklockCache.remove(key);if(lock!nulllock.isHeldByCurrentThread()){lock.unlock();}redisTemplate.delete(lock:device:owner:deviceId);}}七、分布式锁的常见陷阱陷阱现象解决锁过期业务没完成业务跑40秒锁30秒就没了Redisson看门狗 / 预估锁时间释放别人的锁用 value 匹配解决Lua 原子脚本 / RedissonRedis 主从丢锁主节点挂了从节点没有锁数据RedLock 算法 / 容忍偶尔重复死锁持有锁的服务宕机了设置过期时间 / 看门狗7.1 RedLock 算法附争议Redisson 实现了 Redis 官方提出的 RedLock 算法在 N 个独立的 Redis 主节点上同时加锁超过半数N/21成功才算获得锁。这样单节点故障也不会丢锁。但 Martin Kleppmann《设计数据密集型应用》作者对此提出了质疑认为 RedLock 无法解决时钟跳跃等问题。Redis 作者 antirez 也做了回应。个人建议如果业务不是金融级精确计费单节点 Redis 看门狗 合理的过期时间就够了。折腾 RedLock 不如把 Redis 的 Sentinel/Cluster 配置到位。八、完整封装工具类FunctionalInterfacepublicinterfaceLockedTaskT{Texecute()throwsException;}Slf4jComponentpublicclassLockTemplate{AutowiredprivateRedissonClientredissonClient;publicTTexecuteWithLock(Stringkey,LockedTaskTtask){RLocklockredissonClient.getLock(key);try{if(!lock.tryLock(5,30,TimeUnit.SECONDS)){thrownewBizException(获取锁失败请稍后再试);}returntask.execute();}catch(BizExceptione){throwe;}catch(Exceptione){thrownewRuntimeException(e);}finally{if(lock.isHeldByCurrentThread()){lock.unlock();}}}}用的时候一行代码搞定lockTemplate.executeWithLock(lock:order:userId,()-{returnorderMapper.insert(order);});小结分布式锁是微服务并发控制的定海神针。Redisson 凭借看门狗、可重入、公平锁/读写锁等特性是目前最成熟的 Java 分布式锁方案。使用时注意一定要设置过期时间一定要用 try/finally 释放锁一定要验证锁是不是自己持有的。这三条做到了99% 的分布式锁坑你都不会踩。