Redis 分布式锁深度实战

Redis 分布式锁深度实战:Redisson 看门狗、可重入、防死锁完整代码

分布式锁不是 SET NX EX 那么简单。真正踩过生产坑的人都知道:锁过期了业务没跑完怎么办?同一个线程递归调用死锁了怎么办?Redis 宕机锁丢了怎么办?

Redisson 把这些问题全解了。今天从源码级拆透它的三大核心机制:可重入锁、看门狗续期、RedLock 高可用,并给出可直接上生产的完整代码。


一、先搞清楚:分布式锁要解决什么

目标 含义 不做到会怎样
互斥 同一时刻只有一个持有者 数据并发冲突
防死锁 锁必须有过期时间 进程崩溃,锁永远不释放
安全释放 只有持有者才能删自己的锁 A 删了 B 的锁,全线崩溃
可重入 同一线程可多次获取同一把锁 递归调用直接死锁

Redis 单条命令做不到以上全部。Redisson 用 Lua 脚本 + Hash 结构 + 看门狗线程 三板斧,一套打完。


二、可重入锁:同一线程拿锁不阻塞自己

核心数据结构

Redis 里存的不是一个 String,而是一个 Hash

复制代码
复制代码
`Key: myLock
Field: uuid:threadId(客户端唯一标识)
Value: 重入次数(整数)
`

同一个线程第二次拿锁,不是去抢,而是把 Value +1。解锁时 -1,减到 0 才真正 DEL。

加锁 Lua 脚本(源码级)

复制代码

lua

复制代码
`-- KEYS[1] = 锁名
-- ARGV[1] = 租约时长(ms)
-- ARGV[2] = 线程标识(uuid:threadId)

if (redis.call('exists', KEYS[1]) == 0) or 
   (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
    redis.call('hincrby', KEYS[1], ARGV[2], 1)
    redis.call('pexpire', KEYS[1], ARGV[1])
    return nil  -- 加锁成功
end
return redis.call('pttl', KEYS[1])  -- 返回剩余TTL,供客户端等待
`

三行逻辑讲透

  1. 锁不存在 → 新建,计数 = 1,设过期时间
  2. 锁存在且是自己的 → 计数 +1,刷新过期时间(可重入)
  3. 锁存在但不是自己的 → 返回剩余 TTL,别傻等,去订阅通知

解锁 Lua 脚本

复制代码

lua

复制代码
`-- KEYS[1] = 锁名, KEYS[2] = 通知频道
-- ARGV[1] = 线程标识, ARGV[2] = 租约时长

if (redis.call('hexists', KEYS[1], ARGV[1]) == 0) then
    return nil  -- 不是自己的锁,直接忽略
end

local counter = redis.call('hincrby', KEYS[1], ARGV[1], -1)

if counter > 0 then
    redis.call('pexpire', KEYS[1], ARGV[2])
    return 0  -- 还有重入,没真正释放
else
    redis.call('del', KEYS[1])
    redis.call('publish', KEYS[2], ARGV[1])  -- 通知等待者
    return 1  -- 真正释放
end
`

Java 完整代码

复制代码

java

复制代码
`@Configuration
public class RedissonConfig {
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer()
              .setAddress("redis://127.0.0.1:6379")
              .setDatabase(0);
        return Redisson.create(config);
    }
}

@Service
public class OrderService {
    @Resource
    private RedissonClient redissonClient;

    public void createOrder(String orderId) {
        RLock lock = redissonClient.getLock("lock:order:" + orderId);
        try {
            // 最多等10秒,锁30秒后自动过期(不指定leaseTime则启用看门狗)
            boolean acquired = lock.tryLock(10, 30, TimeUnit.SECONDS);
            if (!acquired) {
                throw new RuntimeException("获取锁失败,请重试");
            }
            // 执行业务逻辑
            processOrder(orderId);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            // 谁加的锁谁释放,防止误删
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    private void processOrder(String orderId) {
        // 模拟业务处理
        System.out.println("处理订单: " + orderId);
    }
}
`

可重入演示

复制代码

java

复制代码
`RLock lock = redissonClient.getLock("reentrant:demo");
lock.lock();       // 第1次,计数 = 1
lock.lock();       // 第2次,计数 = 2(同一线程,不阻塞)
lock.unlock();     // 计数 = 1
lock.unlock();     // 计数 = 0,真正释放
`

三、看门狗机制:锁自动续期,业务跑多久锁活多久

问题场景

锁设了 30 秒过期,但业务跑了 35 秒。第 30 秒锁自动释放,B 线程抢到锁,A 还在执行------数据直接乱套。

看门狗怎么解

当你不指定 leaseTime 时,Redisson 默认启动看门狗:

  • 每隔 10 秒(默认 lockWatchdogTimeout = leaseTime / 3)检查一次锁的剩余时间
  • 如果剩余时间 < 1/3 totalLeaseTime,自动通过 PEXPIRE 续期到 30 秒
  • 业务跑多久,锁就活多久
复制代码
复制代码
`时间轴:
0s     → 加锁成功,启动看门狗
10s    → 看门狗检查:剩余20s > 10s,不续期
20s    → 看门狗检查:剩余10s = 1/3,触发续期 → 重置为30s
30s    → 业务还在跑,看门狗再次续期
...
业务结束 → unlock() → 看门狗停止
`

关键代码对比

写法 看门狗 适用场景
lock.lock() ✅ 启用 业务耗时不确定
lock.tryLock(10, 30, SECONDS) ✅ 启用 同上
lock.lock(10, TimeUnit.SECONDS) ❌ 禁用 明确知道业务10秒内完成

避坑:显式传入 leaseTime 时,看门狗不生效。锁到期自动释放,业务必须在租约内完成。


四、RedLock:Redis 宕机也不丢锁

单节点的致命问题

主节点刚写入锁就挂了,数据没同步给从节点。从节点晋升为主节点,锁丢了,其他客户端照样能加锁------互斥性被打破

RedLock 算法

N 个独立 Redis 节点 (通常 5 个)同时申请锁,获取 多数(≥ N/2 + 1) 即算成功。

复制代码
复制代码
`5 节点场景:
- 客户端同时向 5 个节点发送加锁请求
- 成功 ≥ 3 个 → 加锁成功
- 成功 < 3 个 → 向所有节点释放已获取的锁
`

RedLock 完整代码

复制代码

java

复制代码
`Config config = new Config();
config.useClusterServers()
      .addNodeAddress(
          "redis://127.0.0.1:6379",
          "redis://127.0.0.1:6380",
          "redis://127.0.0.1:6381"
      );

RedissonClient redisson = Redisson.create(config);

RLock lock1 = redisson.getLock("lock1");
RLock lock2 = redisson.getLock("lock2");
RLock lock3 = redisson.getLock("lock3");

RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);

try {
    redLock.lock();  // 阻塞等待,直到多数节点加锁成功
    // 执行业务逻辑
    processCriticalTask();
} finally {
    redLock.unlock();
}
`

RedLock 的代价

优势 代价
抗单节点故障 运维成本高(5个独立实例)
强一致性保障 依赖系统时钟,时钟跳变会出问题
高可用 加锁需访问多节点,延迟更高

五、其他锁类型速查

锁类型 适用场景 核心代码
公平锁 防止线程饥饿,先到先得 RFairLock fairLock = redisson.getFairLock("myFairLock");
读写锁 多读少写 RReadWriteLock rw = redisson.getReadWriteLock("rw");
联锁 多个锁必须同时成功 new RedissonMultiLock(lock1, lock2).lock();
异步锁 高并发不阻塞 I/O 线程 lock.tryLockAsync(10, 30, SECONDS).thenAccept(...)

六、生产避坑清单

解法
锁过期业务没跑完 不指定 leaseTime,启用看门狗
误删别人的锁 unlock 前必查 isHeldByCurrentThread()
中断异常锁没释放 finally 块里释放,catch 里 Thread.currentThread().interrupt()
虚拟线程下重入计数不同步 Redisson 3.23+ 已修复,升级版本
Redis 集群下脚本跨 slot 失败 Key 用 {segment}myLock 格式,保证同 slot
锁超时时间怎么设 压测取平均值 × 3~5 倍,给 GC 和网络抖动留余量

总结:一张图记住 Redisson 锁的全貌

复制代码
复制代码
`Redisson 分布式锁
├── 可重入:Hash 结构存 (uuid:threadId → 计数)
├── 防死锁:EXPIRE 过期 + 看门狗自动续期
├── 防误删:Lua 脚本原子校验身份后再 DEL
├── 高可用:RedLock 多节点多数派
└── 锁类型:RLock / RFairLock / RReadWriteLock / RedissonMultiLock / RedissonRedLock
`

Redisson 不是银弹,但它是目前 Java 生态里实现 Redis 分布式锁最完整、最成熟的方案。理解这套机制,比背十道面试题有用得多。