分布式锁实战:防重下单、设备独占控制
2026/8/1 18:23:06 网站建设 项目流程

分布式锁实战:防重下单、设备独占控制

同事写了个秒杀接口,测试时手速过快连点了20下鼠标,结果一个用户下了20单。他转头问我:“哥,咋防重?” 我说:“兄弟,你需要一把锁——不是锁门的那种,是锁并发的。”

一、分布式锁的应用场景

在单机时代,synchronizedReentrantLock就能搞定并发问题。但微服务是多实例部署的,JVM 锁只能控制当前进程,跨进程就管不到了。这时候需要一把能横跨所有服务实例的锁——分布式锁。

典型场景:

场景说明不锁的后果
防重下单用户快速点击多次扣钱多次,只出一次货
库存扣减并发减库存超卖,库存变负数
设备独占同一设备同一时间只能被一个用户操作A用户的操作被B用户覆盖
定时任务防重多实例同时跑定时任务重复执行,数据错乱

二、四种分布式锁方案对比

方案原理优点缺点
DB 唯一索引INSERT 冲突 → 加锁失败实现简单性能差,无过期释放
Redis SETNXSET key value NX EX高性能,易用锁过期业务没完
Redisson基于Redis的高层封装功能最全依赖Redis高可用
ZooKeeper临时顺序节点CP一致性极强性能不如Redis,维护复杂

推荐:业务场景优先选 Redisson(Redis 封装),因为大多数系统本来就有 Redis,开箱即用。除非对一致性要求极高(如金融资金清算),否则没必要上 ZooKeeper。

三、原生 Redis 实现分布式锁

// 加锁StringlockKey="lock:order:"+userId;Booleanlocked=redisTemplate.opsForValue().setIfAbsent(lockKey,uuid,30,TimeUnit.SECONDS);// NX + EXif(!Boolean.TRUE.equals(locked)){thrownewBizException("操作太频繁,请稍后再试");}// 释放锁 —— 必须用 Lua 脚本保证原子性!Stringlua="if 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(){Configconfig=newConfig();config.useSingleServer().setAddress("redis://127.0.0.1:6379");returnRedisson.create(config);}}

4.1 看门狗(Watchdog)自动续期

Redisson 最核心的机制。你设置锁过期 30 秒,如果 20 秒后业务还在跑,看门狗自动帮你续到 30 秒。业务完成后锁自动释放,不会长期占用。

RLocklock=redissonClient.getLock("lock:order:"+userId);lock.lock();// 默认30秒过期,看门狗每10秒续一次try{// 业务逻辑,随便执行多久}finally{lock.unlock();// 看门狗停止续期}

4.2 tryLock:带超时的非阻塞加锁

booleanlocked=lock.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 公平锁/读写锁

// 公平锁:按申请顺序排队获取RFairLockfairLock=redissonClient.getFairLock("lock:order");// 读写锁:读并发、写互斥RReadWriteLockrwLock=redissonClient.getReadWriteLock("lock:device");rwLock.readLock().lock();// 多个读操作可以并发rwLock.writeLock().lock();// 写操作互斥

读写锁非常适合"设备状态读取频繁,但修改低频"的场景。

五、实战1:防重下单

@ServicepublicclassOrderService{@AutowiredprivateRedissonClientredissonClient;@TransactionalpublicOrdercreateOrder(LonguserId,List<OrderItem>items){StringlockKey="lock:order:user:"+userId;RLocklock=redissonClient.getLock(lockKey);booleanacquired=false;try{acquired=lock.tryLock(3,30,TimeUnit.SECONDS);if(!acquired){thrownewBizException("请勿重复提交订单");}// 幂等性检查:30秒内同一用户的重复请求StringidempotentKey="order:submit:"+userId+":"+requestHash(items);Booleanexists=redisTemplate.opsForValue().setIfAbsent(idempotentKey,"1",30,TimeUnit.SECONDS);if(!Boolean.TRUE.equals(exists)){thrownewBizException("订单已提交,请勿重复操作");}// 创建订单...returnorder;}finally{if(acquired&&lock.isHeldByCurrentThread()){lock.unlock();}}}}

双层防护:外层分布式锁(防并发),内层幂等键(防重复提交)。两者结合才是真正可靠的防重方案。

六、实战2:设备独占控制

在物联网/无人售货柜场景中,一台设备同一时间只能被一个操作员操作,否则指令冲突导致状态混乱。

@ComponentpublicclassDeviceLockManager{@AutowiredprivateRedissonClientredissonClient;privatefinalConcurrentHashMap<String,RLock>lockCache=newConcurrentHashMap<>();publicbooleanacquireDevice(LongdeviceId,LongoperatorId,inttimeoutSec){Stringkey="lock:device:"+deviceId;RLocklock=redissonClient.getLock(key);booleanacquired=lock.tryLock();if(acquired){lockCache.put(key,lock);// 记录谁持有了这把锁redisTemplate.opsForValue().set("lock:device:owner:"+deviceId,String.valueOf(operatorId),Duration.ofSeconds(timeoutSec));}returnacquired;}publicvoidreleaseDevice(LongdeviceId){Stringkey="lock:device:"+deviceId;RLocklock=lockCache.remove(key);if(lock!=null&&lock.isHeldByCurrentThread()){lock.unlock();}redisTemplate.delete("lock:device:owner:"+deviceId);}}

七、分布式锁的常见陷阱

陷阱现象解决
锁过期业务没完成业务跑40秒,锁30秒就没了Redisson看门狗 / 预估锁时间
释放别人的锁用 value 匹配解决Lua 原子脚本 / Redisson
Redis 主从丢锁主节点挂了,从节点没有锁数据RedLock 算法 / 容忍偶尔重复
死锁持有锁的服务宕机了设置过期时间 / 看门狗

7.1 RedLock 算法(附争议)

Redisson 实现了 Redis 官方提出的 RedLock 算法:在 N 个独立的 Redis 主节点上同时加锁,超过半数(N/2+1)成功才算获得锁。这样单节点故障也不会丢锁。

但 Martin Kleppmann(《设计数据密集型应用》作者)对此提出了质疑,认为 RedLock 无法解决时钟跳跃等问题。Redis 作者 antirez 也做了回应。

个人建议:如果业务不是金融级精确计费,单节点 Redis + 看门狗 + 合理的过期时间就够了。折腾 RedLock 不如把 Redis 的 Sentinel/Cluster 配置到位。

八、完整封装工具类

@FunctionalInterfacepublicinterfaceLockedTask<T>{Texecute()throwsException;}@Slf4j@ComponentpublicclassLockTemplate{@AutowiredprivateRedissonClientredissonClient;public<T>TexecuteWithLock(Stringkey,LockedTask<T>task){RLocklock=redissonClient.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% 的分布式锁坑你都不会踩。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询