Redisson分布式锁的深度解析

摘要:从一次"分布式锁莫名其妙失效"的故障出发,深度剖析Redisson分布式锁的5大核心机制。通过Watch Dog自动续期原理、可重入锁的实现、红锁Redlock的争议、以及公平锁与联锁的使用场景,揭秘Redisson如何解决锁过期、死锁、主从切换等问题。配合源码分析和时序图展示加锁解锁流程,手写简易版Watch Dog机制,给出生产环境的最佳实践和避坑指南。


💥 翻车现场

周五下午,哈吉米收到了一个诡异的bug报告。

测试同学:@哈吉米 秒杀功能有问题!有时候会超卖!
哈吉米:不可能啊,我用了Redisson分布式锁!
测试同学:你自己看日志!

查看日志:

2024-10-07 15:23:45 [Thread-1] 获取锁成功,库存=10
2024-10-07 15:23:45 [Thread-2] 获取锁成功,库存=10  ← 两个线程同时获取到锁?
2024-10-07 15:23:46 [Thread-1] 扣减库存,库存=9
2024-10-07 15:23:46 [Thread-2] 扣减库存,库存=9  ← 都扣成9了,应该是8

哈吉米:"卧槽,两个线程同时获取到锁了?"

查看代码:

RLock lock = redissonClient.getLock("lock:stock:" + productId);

try {
    lock.lock();  // 获取锁
    
    // 业务逻辑
    Stock stock = stockMapper.selectById(productId);
    stock.setNum(stock.getNum() - 1);
    stockMapper.updateById(stock);
    
} finally {
    lock.unlock();  // 释放锁
}

哈吉米:"代码没问题啊……"

紧急查看Redis集群配置:

# Redis集群:1主2从
spring:
  redis:
    cluster:
      nodes:
        - 192.168.1.10:6379  # 主节点
        - 192.168.1.11:6379  # 从节点1
        - 192.168.1.12:6379  # 从节点2

南北绿豆和阿西噶阿西赶来了。

南北绿豆:"你遇到了Redis主从异步复制的问题!"
哈吉米:"???"
阿西噶阿西:"来,我给你深度解析Redisson的原理和坑。"


🤔 核心机制1:Redisson的加锁流程

加锁的Lua脚本(源码)

-- Redisson加锁的Lua脚本
-- KEYS[1]: 锁的key
-- ARGV[1]: 锁的过期时间(30秒)
-- ARGV[2]: 锁的value(UUID:ThreadID)

-- 1. 判断锁是否存在
if (redis.call('exists', KEYS[1]) == 0) then
    -- 锁不存在,获取锁
    redis.call('hset', KEYS[1], ARGV[2], 1);  -- 设置重入次数=1
    redis.call('pexpire', KEYS[1], ARGV[1]);  -- 设置过期时间
    return nil;  -- 获取成功
end;

-- 2. 判断是否是当前线程持有的锁(可重入)
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
    -- 是当前线程,重入次数+1
    redis.call('hincrby', KEYS[1], ARGV[2], 1);
    redis.call('pexpire', KEYS[1], ARGV[1]);  -- 刷新过期时间
    return nil;  -- 重入成功
end;

-- 3. 锁被其他线程持有,返回锁的剩余过期时间
return redis.call('pttl', KEYS[1]);

存储结构

Redis中的锁(Hash结构):

key: lock:stock:1001

value (Hash):
{
  "uuid-123:thread-1": 1  ← 重入次数
}

TTL: 30秒

加锁流程图

sequenceDiagram
    participant Thread1 as 线程1
    participant Redis
    participant Thread2 as 线程2

    Thread1->>Redis: 1. 执行Lua脚本(加锁)
    Note over Redis: exists(lock) == 0?
    Redis->>Redis: 2. hset(lock, uuid1:thread1, 1)
    Redis->>Redis: 3. pexpire(lock, 30000ms)
    Redis->>Thread1: 返回nil(获取成功)✅
    
    Thread2->>Redis: 4. 执行Lua脚本(加锁)
    Note over Redis: exists(lock) == 0? 否
    Note over Redis: hexists(lock, uuid2:thread2)? 否
    Redis->>Thread2: 返回29500(锁的剩余时间)
    Note over Thread2: 等待或重试
    
    Note over Thread1: 执行业务逻辑
    
    Thread1->>Redis: 5. 执行Lua脚本(解锁)
    Redis->>Redis: 6. hincrby(lock, uuid1:thread1, -1)
    Note over Redis: 重入次数 = 0,删除锁
    Redis->>Redis: 7. del(lock)
    Redis->>Thread1: 返回1(释放成功)
    
    Thread2->>Redis: 8. 执行Lua脚本(加锁)
    Redis->>Thread2: 返回nil(获取成功)✅

南北绿豆:"看到了吗?Redisson用Hash结构存储锁,支持可重入!"


🤔 核心机制2:Watch Dog自动续期

问题场景

场景:
1. 获取锁,过期时间30秒
2. 业务逻辑执行了40秒(比如调用外部接口慢)
3. 30秒时,锁自动过期
4. 其他线程获取到锁
5. 两个线程同时执行业务 ❌

Watch Dog机制

原理:定时任务自动续期

// Redisson源码(简化)
public class RedissonLock {
    
    private long internalLockLeaseTime = 30000;  // 默认30秒
    
    public void lock() {
        // 1. 获取锁
        tryLockInner(internalLockLeaseTime);
        
        // 2. 启动Watch Dog
        scheduleExpirationRenewal();
    }
    
    private void scheduleExpirationRenewal() {
        // 每10秒执行一次(30秒 / 3 = 10秒)
        Timeout task = commandExecutor.getConnectionManager()
            .newTimeout(new TimerTask() {
                @Override
                public void run(Timeout timeout) {
                    // 续期:重置过期时间为30秒
                    renewExpiration();
                    
                    // 继续调度下一次
                    scheduleExpirationRenewal();
                }
            }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
    }
    
    private void renewExpiration() {
        // Lua脚本:如果锁还存在,续期到30秒
        redis.call('pexpire', KEYS[1], 30000);
    }
}

Watch Dog时序图

sequenceDiagram
    participant App as 应用线程
    participant Redis
    participant WatchDog as Watch Dog定时任务

    App->>Redis: 1. 获取锁,TTL=30秒
    Redis->>App: 成功 ✅
    App->>WatchDog: 2. 启动Watch Dog
    
    par 业务执行
        App->>App: 执行业务逻辑(40秒)
    and Watch Dog续期
        loop 每10秒
            Note over WatchDog: 10秒后
            WatchDog->>Redis: PEXPIRE lock 30000
            Note over Redis: 续期成功,TTL重置为30秒
            
            Note over WatchDog: 20秒后
            WatchDog->>Redis: PEXPIRE lock 30000
            
            Note over WatchDog: 30秒后
            WatchDog->>Redis: PEXPIRE lock 30000
            
            Note over WatchDog: 40秒后(业务完成)
        end
    end
    
    App->>Redis: 3. 释放锁(DEL)
    App->>WatchDog: 4. 停止Watch Dog

关键点

Watch Dog续期频率:
过期时间 / 3 = 30秒 / 3 = 10秒

时间轴:
T0: 获取锁,TTL=30秒
T10: Watch Dog续期,TTL=30秒
T20: Watch Dog续期,TTL=30秒
T30: Watch Dog续期,TTL=30秒
T40: 业务完成,释放锁,停止Watch Dog

结果:即使业务执行40秒,锁也不会过期

阿西噶阿西:"这就是Redisson的核心优势——自动续期,不怕业务执行慢!"


如何关闭Watch Dog?

// 方法1:指定leaseTime(手动设置过期时间)
RLock lock = redissonClient.getLock("lock:stock:1001");
lock.lock(30, TimeUnit.SECONDS);  // 指定30秒,不会自动续期

// 方法2:tryLock指定leaseTime
lock.tryLock(10, 30, TimeUnit.SECONDS);
// 等待时间10秒,锁过期30秒,不会自动续期

注意:指定leaseTime后,Watch Dog不会启动,锁到期自动释放。


🤔 核心机制3:可重入锁

什么是可重入?

RLock lock = redissonClient.getLock("lock:test");

public void method1() {
    lock.lock();
    try {
        method2();  // 调用method2
    } finally {
        lock.unlock();
    }
}

public void method2() {
    lock.lock();  // 同一个线程再次获取锁(可重入)✅
    try {
        doSomething();
    } finally {
        lock.unlock();
    }
}

如果不可重入

method1获取锁 → 调用method2 → method2尝试获取锁 → 等待method1释放锁
→ 死锁 ❌

Redisson可重入的实现

Redis存储

key: lock:test

value (Hash):
{
  "uuid-123:thread-1": 2  ← 重入次数=2(method1加锁1次,method2加锁1次)
}

加锁Lua脚本

-- 如果锁存在,且是当前线程持有
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
    -- 重入次数+1
    redis.call('hincrby', KEYS[1], ARGV[2], 1);
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return nil;
end;

解锁Lua脚本

-- 解锁时,重入次数-1
local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1);

if (counter > 0) then
    -- 重入次数 > 0,不删除锁,只刷新过期时间
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return 0;
else
    -- 重入次数 = 0,删除锁
    redis.call('del', KEYS[1]);
    return 1;
end;

可重入流程图

graph TD
    A[method1: lock.lock] --> B[Hash: uuid:thread-1 = 1]
    B --> C[执行method1业务]
    C --> D[调用method2]
    D --> E[method2: lock.lock]
    E --> F{锁是否存在?}
    F -->|是| G{是当前线程持有?}
    G -->|是| H[重入次数+1: uuid:thread-1 = 2]
    H --> I[执行method2业务]
    I --> J[method2: lock.unlock]
    J --> K[重入次数-1: uuid:thread-1 = 1]
    K --> L{重入次数 > 0?}
    L -->|是| M[不删除锁,继续]
    M --> N[method1: lock.unlock]
    N --> O[重入次数-1: uuid:thread-1 = 0]
    O --> P[删除锁 ✅]
    
    style H fill:#90EE90
    style P fill:#90EE90

哈吉米:"原来可重入锁是通过计数器实现的!"


🤔 核心机制4:红锁(Redlock)的争议

为什么需要Redlock?

问题:Redis主从异步复制导致锁丢失

场景:
T1: 客户端A在主节点获取锁 ✅
T2: 主节点宕机(锁还没同步到从节点)
T3: 从节点升级为主节点(没有锁记录)
T4: 客户端B在新主节点获取锁 ✅
T5: A和B同时持有锁 ❌

时序图

sequenceDiagram
    participant ClientA as 客户端A
    participant Master as 主节点
    participant Slave as 从节点
    participant ClientB as 客户端B

    ClientA->>Master: 1. SET lock:key uuid-A NX EX 30
    Master->>ClientA: OK(获取锁)✅
    
    Note over Master: 还没同步到从节点
    
    rect rgb(255, 182, 193)
        Note over Master: 主节点宕机
    end
    
    Note over Slave: 从节点升级为主节点
    
    ClientB->>Slave: 2. SET lock:key uuid-B NX EX 30
    Note over Slave: 从节点没有锁记录
    Slave->>ClientB: OK(获取锁)✅
    
    Note over ClientA,ClientB: 两个客户端同时持有锁 ❌

Redlock算法

原理:向N个独立的Redis实例(不是主从关系)获取锁,超过半数成功才算成功。

// Redisson的Redlock实现
RLock lock1 = redisson1.getLock("lock:stock:1001");
RLock lock2 = redisson2.getLock("lock:stock:1001");
RLock lock3 = redisson3.getLock("lock:stock:1001");
RLock lock4 = redisson4.getLock("lock:stock:1001");
RLock lock5 = redisson5.getLock("lock:stock:1001");

// 红锁(需要超过半数成功)
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3, lock4, lock5);

try {
    // 尝试获取锁
    boolean success = redLock.tryLock(10, 30, TimeUnit.SECONDS);
    if (success) {
        // 业务逻辑
        doSeckill();
    }
} finally {
    redLock.unlock();
}

流程

1. 向5个Redis实例获取锁
2. 成功3个以上(超过半数) → 获取锁成功
3. 失败或成功不足半数 → 获取锁失败
4. 释放锁时,向所有实例释放

Redlock的争议

支持方(Redis作者 Antirez)

  • ✅ 即使部分节点宕机,锁仍然有效
  • ✅ 不依赖主从复制

反对方(分布式专家 Martin Kleppmann)

  • ❌ 时钟漂移问题(不同服务器时钟不一致)
  • ❌ 长时间GC导致锁失效
  • ❌ 复杂度高,不如用Zookeeper

阿西噶阿西:"Redlock在工业界用得不多,争议太大,不如用Zookeeper或单Redis + 主从哨兵。"


🤔 核心机制5:公平锁

公平锁 vs 非公平锁

非公平锁(默认):

RLock lock = redissonClient.getLock("lock:test");
lock.lock();  // 非公平,新来的线程可以抢锁

公平锁

RLock lock = redissonClient.getFairLock("lock:test");
lock.lock();  // 公平,严格按FIFO顺序

公平锁的实现

原理:用Redis的List存储等待队列

Redis存储:

key: redisson_lock_queue:{lock:test}

value (List):
["uuid1:thread-1", "uuid2:thread-2", "uuid3:thread-3"]
 ↑ 队列头(下一个获取锁)

流程

1. 线程1加入队列:RPUSH queue uuid1
2. 线程2加入队列:RPUSH queue uuid2
3. 线程3加入队列:RPUSH queue uuid3
4. 检查队列头是否是自己(LINDEX queue 0)
5. 是 → 获取锁
6. 否 → 订阅锁释放事件,等待

性能对比

锁类型TPS公平性
非公平锁8000
公平锁5000

性能差距:公平锁慢37%


🎯 Redisson的其他高级功能

功能1:联锁(MultiLock)

场景:需要同时锁住多个资源

RLock lock1 = redissonClient.getLock("lock:user:10086");
RLock lock2 = redissonClient.getLock("lock:user:10087");

// 联锁(同时获取两把锁)
RedissonMultiLock multiLock = new RedissonMultiLock(lock1, lock2);

try {
    multiLock.lock();
    // 同时锁住了两个用户,可以安全地转账
    transfer(10086, 10087, 100);
} finally {
    multiLock.unlock();
}

优点

  • ✅ 防止死锁(统一加锁顺序)
  • ✅ 原子性(要么全部获取,要么全部失败)

功能2:读写锁(ReadWriteLock)

RReadWriteLock rwLock = redissonClient.getReadWriteLock("lock:data");

// 读锁(共享锁,多个线程可以同时持有)
RLock readLock = rwLock.readLock();
readLock.lock();
try {
    // 读数据
    readData();
} finally {
    readLock.unlock();
}

// 写锁(排他锁,只有一个线程能持有)
RLock writeLock = rwLock.writeLock();
writeLock.lock();
try {
    // 写数据
    writeData();
} finally {
    writeLock.unlock();
}

适用场景:读多写少


功能3:信号量(Semaphore)

RSemaphore semaphore = redissonClient.getSemaphore("semaphore:limit");

// 设置许可数量(最多3个线程)
semaphore.trySetPermits(3);

// 获取许可
semaphore.acquire();
try {
    // 业务逻辑(最多3个线程同时执行)
    doSomething();
} finally {
    semaphore.release();
}

适用场景:限流


🛡️ Redisson使用的7个最佳实践

实践1:设置合理的锁过期时间

// ❌ 错误(过期时间太短)
lock.lock(5, TimeUnit.SECONDS);  // 5秒,业务可能执行不完

// ✅ 正确(让Watch Dog自动续期)
lock.lock();  // 默认30秒,自动续期

// 或者
lock.tryLock(10, 30, TimeUnit.SECONDS);  // 等待10秒,锁30秒

实践2:必须在finally中释放锁

RLock lock = redissonClient.getLock("lock:test");

try {
    lock.lock();
    doSomething();
} finally {
    if (lock.isHeldByCurrentThread()) {  // 判断是否是当前线程持有
        lock.unlock();
    }
}

实践3:避免锁的粒度太大

// ❌ 错误(锁粒度太大)
RLock lock = redissonClient.getLock("lock:order");  // 锁住所有订单

// ✅ 正确(按订单ID锁)
RLock lock = redissonClient.getLock("lock:order:" + orderId);

实践4:监控锁的性能

@Scheduled(fixedDelay = 10000)
public void monitorLock() {
    RLock lock = redissonClient.getLock("lock:stock:1001");
    
    log.info("锁是否被持有: {}", lock.isLocked());
    log.info("锁的剩余时间: {}ms", lock.remainTimeToLive());
}

实践5:设置获取锁的超时时间

// ❌ 错误(无限等待)
lock.lock();  // 如果锁一直被占用,永远等待

// ✅ 正确(超时返回)
boolean success = lock.tryLock(3, TimeUnit.SECONDS);
if (!success) {
    return Result.error("系统繁忙,请稍后再试");
}

实践6-7(快速总结)

  1. 区分业务场景(秒杀用非公平锁,转账用公平锁)
  2. Redis集群用哨兵模式(高可用)

🎓 面试标准答案

题目:Redisson分布式锁的核心原理是什么?

答案

核心机制

1. Lua脚本保证原子性

  • 加锁:SET NX EX + Hash存储重入次数
  • 解锁:判断持有者 + 重入次数-1

2. Watch Dog自动续期

  • 默认30秒过期
  • 每10秒续期一次
  • 业务执行完停止续期

3. 可重入

  • Hash结构存储重入次数
  • 同一线程多次加锁,计数器+1
  • 解锁时计数器-1,为0时删除锁

4. UUID标识

  • 每个锁有唯一标识(UUID:ThreadID)
  • 防止误删别人的锁

5. 公平锁支持

  • List存储等待队列
  • 严格按FIFO顺序

优点

  • 性能好(8000 TPS)
  • 自动续期(不怕业务慢)
  • 功能丰富(可重入、公平锁、读写锁)

缺点

  • AP模型(主从切换可能丢锁)

🎉 结束语

晚上10点,哈吉米把代码改成了Redisson。

哈吉米:"用Redisson后,超卖问题解决了!而且Watch Dog自动续期,再也不怕业务执行慢了!"

南北绿豆:"对,Redisson是Redis分布式锁的最佳实践。"

阿西噶阿西:"记住:不要自己实现Redis锁,直接用Redisson,功能完善、久经考验。"

哈吉米:"还有可重入机制,用Hash存储重入次数,很巧妙!"

南北绿豆:"对,理解了Redisson的原理,就理解了分布式锁的最佳实践!"


记忆口诀

Redisson分布式锁强,Lua脚本保原子
Watch Dog自动续期,业务再慢也不怕
Hash结构存重入,计数加减很巧妙
UUID标识防误删,公平锁队列排
主从切换可能丢,红锁争议不推荐


希望这篇文章能帮你彻底掌握Redisson分布式锁!记住:生产环境直接用Redisson,不要自己造轮子!💪