
为什么要引入Redisson——Redis SETNX加锁的四大问题前置知识SETNX加锁的基本实现在使用Redis做分布式锁时最原始的做法就是用SETNXSET if Not eXists命令// 加锁 Boolean locked redisTemplate.opsForValue() .setIfAbsent(lock:order:123, thread-1, 10, TimeUnit.SECONDS); // 释放锁 redisTemplate.delete(lock:order:123);这个方案看似简单但在生产环境中存在四个致命问题Redisson 就是专为解决这些问题而生的。问题一不可重入什么是不可重入假设你有一个业务方法methodA()它获取了一把锁然后在内部又调用了methodB()而methodB()也需要获取同一把锁Transactional public void methodA() { // 获取锁 lock:order:123 lock(lock:order:123); // 执行业务逻辑... methodB(); // 内部也要获取同一把锁 // 释放锁 unlock(lock:order:123); } public void methodB() { lock(lock:order:123); // ❌ 获取失败锁已经被自己持有 // 执行业务逻辑... unlock(lock:order:123); }用 SETNX 的实现methodA已经持有了锁methodB再尝试 SETNX 同一个Key时会返回false——自己锁住了自己这就是不可重入。可重入锁的含义是同一个线程可以多次获取同一把锁不会被自己阻塞。Java 的ReentrantLock就是可重入的但 SETNX 做不到。Redisson 怎么解决Redisson 内部用Hash 结构存储锁信息Key: lock:order:123 Value: { thread-id-1: 2 ← 持有者 重入次数 }字段名是线程标识UUID 线程ID字段值是重入计数器同一线程再次加锁时判断字段名匹配 → 计数器1释放锁时计数器-1减到0才真正删除Key。这套逻辑完全由 Lua 脚本保证原子性。# 第一次加锁 HSET lock:order:123 uuid:thread-1 1 # 重入加锁 HINCRBY lock:order:123 uuid:thread-1 1 → 值变为2 # 释放一次 HINCRBY lock:order:123 uuid:thread-1 -1 → 值变为1 # 再释放一次值为0删除Key HINCRBY lock:order:123 uuid:thread-1 -1 → 值为0 → DEL lock:order:123问题二不可重试SETNX 加锁失败后怎么办用 SETNX 加锁失败意味着锁被别人持有。你只有两个选择// 选择1直接放弃 if (!locked) { return null; // 业务直接失败 } // 选择2自己写while循环重试 while (!locked) { Thread.sleep(50); locked redisTemplate.opsForValue().setIfAbsent(...); }直接放弃 → 业务失败率飙升自己写循环 → 控制不好就变成自旋空转浪费CPU也没有最大重试次数、超时控制等机制。Redisson 怎么解决Redisson 提供了两种优雅的重试策略1. tryLock 带超时参数RLock lock redisson.getLock(lock:order:123); // 等待最多3秒锁持有最多30秒 boolean locked lock.tryLock(3, 30, TimeUnit.SECONDS);内部实现是Pub/Sub 信号量机制加锁失败时不会自旋空转而是通过 Redis 的发布订阅监听锁释放消息一旦锁释放就立刻被唤醒再次尝试既高效又不浪费CPU。2. lock 阻塞等待RLock lock redisson.getLock(lock:order:123); lock.lock(); // 一直等直到拿到锁底层同样是 Pub/Sub 驱动不会忙等。问题三超时释放锁提前释放的危险假设业务执行需要15秒但你设了10秒的锁过期时间时间线 0s 线程A 获取锁过期时间10秒 10s 锁自动过期释放 ← 业务还没执行完 10s 线程B 获取锁 15s 线程A 执行完删除锁 ← 把线程B的锁删了 此时线程B以为锁还在实际已被释放这就导致了两个严重后果锁提前释放→ 并发安全问题多个线程同时进入临界区误删别人的锁→ 后续锁全部失效那你可能说把过期时间设长一点不就行了但设太长又会导致持锁线程宕机后锁长时间不释放其他线程全部阻塞。Redisson 怎么解决——看门狗机制WatchdogRedisson 引入了看门狗Watchdog自动续期机制RLock lock redisson.getLock(lock:order:123); lock.lock(); // 不指定过期时间看门狗生效核心逻辑如果lock.lock()不指定 leaseTime看门狗就会启动默认锁的过期时间为30秒lockWatchdogTimeout每隔10秒过期时间的1/3看门狗会自动把锁的过期时间重置为30秒只要持有锁的线程还活着锁就永远不会过期线程宕机后看门狗停止续期锁30秒后自动释放时间线看门狗生效 0s 线程A获取锁过期时间30秒 10s 看门狗续期 → 过期时间重置为30秒 20s 看门狗续期 → 过期时间重置为30秒 30s 看门狗续期 → 过期时间重置为30秒 ... 只要线程A活着锁就不会过期另外Redisson 在释放锁时还会验证锁的持有者身份只会删除自己持有的锁不会误删别人的锁。这一切都由 Lua 脚本保证原子性。问题四主从一致性Redis主从切换导致锁丢失这是最隐蔽也最危险的问题。在 Redis 主从架构下正常情况 客户端A → 主节点加锁lock:order:123 thread-1 主节点 → 异步复制 → 从节点 异常情况 1. 客户端A 在主节点加锁成功 2. 主节点还没来得及把锁数据同步给从节点 3. 主节点宕机 4. 从节点被提升为新主节点 5. 新主节点上没有锁数据 → 锁丢失了 6. 客户端B 来加锁 → 成功 7. 客户端A 和 客户端B 同时持有锁 → 并发安全问题问题的根源在于Redis 主从复制是异步的主节点写入成功就返回不等待从节点同步完成。主节点宕机时尚未同步的数据就会丢失。Redisson 怎么解决——RedLock算法Redisson 实现了 Redis 官方提出的RedLockRed Lock算法核心思想是不在单个节点上加锁而是在多个独立的Redis节点上同时加锁。RedLock 加锁流程 1. 获取当前时间 T1 2. 依次向 5 个独立的 Redis 节点发送加锁请求 3. 计算加锁成功数和耗时T2 - T1 4. 如果 成功数 ≥ 3多数派且 耗时 锁过期时间 → 加锁成功 5. 否则向所有节点发送释放锁请求 → 加锁失败RLock lock1 redisson1.getLock(lock:order:123); RLock lock2 redisson2.getLock(lock:order:123); RLock lock3 redisson3.getLock(lock:order:123); RedissonRedLock redLock new RedissonRedLock(lock1, lock2, lock3); redLock.lock(); // 业务逻辑... redLock.unlock();5个节点中只要有3个加锁成功就认为锁获取成功。即使某个节点宕机导致锁丢失其他节点上仍然有锁数据不会出现两个客户端同时持有锁的情况。⚠️补充说明RedLock 也存在争议Martin Kleppmann 曾撰文批评其可靠性Redis 作者 Antirez 也做了回应。在生产环境中如果对一致性要求极高建议考虑 ZooKeeper 或 etcd 等基于共识协议的分布式锁方案。但对于大多数互联网场景Redisson 的单节点锁 看门狗已经足够可靠。总结四问题 vs 四方案问题SETNX的表现Redisson的方案不可重入同线程重入会自锁Hash结构 重入计数器不可重试加锁失败只能放弃或自旋Pub/Sub信号量 tryLock超时重试超时释放业务未完成锁就过期还可能误删别人的锁看门狗自动续期 Lua脚本安全释放主从一致性主节点宕机导致锁丢失RedLock多节点多数派加锁一句话总结SETNX只是一个命令而Redisson是一套完整的分布式锁框架它用Hash结构解决重入、用Pub/Sub解决重试、用看门狗解决续期、用RedLock解决一致性并且所有操作都通过Lua脚本保证原子性。这就是为什么在生产环境中我们几乎不会裸用SETNX而是选择Redisson。