【Redis】笔记|第5节|Redisson实现高并发分布式锁核心源码

发布于:2025-06-03 ⋅ 阅读:(24) ⋅ 点赞:(0)

一、加锁流程

1. 核心方法调用链
RLock lock = redisson.getLock("resource");
lock.lock(); // 阻塞式加锁
  ↳ lockInterruptibly()
    ↳ tryAcquire(-1, leaseTime, unit) // leaseTime=-1表示启用看门狗
      ↳ tryAcquireAsync()
        ↳ tryLockInnerAsync() // 执行Lua脚本
2. Lua脚本实现(关键)
// RedissonLock.tryLockInnerAsync()
"if (redis.call('exists', KEYS[1]) == 0) then " +  // 锁不存在
    "redis.call('hset', KEYS[1], ARGV[2], 1); " +  // 创建锁(Hash结构)
    "redis.call('pexpire', KEYS[1], ARGV[1]); " +  // 设置过期时间
    "return nil; " +
"end; " +
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +  // 锁已存在,判断是否重入
    "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +  // 重入次数+1
    "redis.call('pexpire', KEYS[1], ARGV[1]); " +  // 重置过期时间
    "return nil; " +
"end; " +
"return redis.call('pttl', KEYS[1]);",  // 返回剩余时间,加锁失败
Collections.singletonList(getName()),  // KEYS[1]: 锁名称(如"resource")
internalLockLeaseTime, getLockName(threadId)  // ARGV[1]: 过期时间;ARGV[2]: 线程标识(UUID:threadId)
3. 关键点
  • 原子性:通过Lua脚本保证检查锁和创建锁的原子性。
  • 数据结构:使用Redis的Hash存储锁信息,field为线程标识,value为重入次数。
  • 过期时间:默认30秒(看门狗机制自动续期),防止死锁。

二、解锁流程

1. 核心方法调用链
lock.unlock();
  ↳ unlockAsync()
    ↳ unlockInnerAsync() // 执行Lua脚本
2. Lua脚本实现(关键)
// RedissonLock.unlockInnerAsync()
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +  // 锁不存在或非当前线程持有
    "return nil; " +
"end; " +
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +  // 重入次数-1
"if (counter > 0) then " +  // 重入次数>0,继续持有锁
    "redis.call('pexpire', KEYS[1], ARGV[2]); " +  // 重置过期时间
    "return 0; " +
"else " +  // 重入次数=0,释放锁
    "redis.call('del', KEYS[1]); " +  // 删除锁
    "redis.call('publish', KEYS[2], ARGV[1]); " +  // 发布锁释放消息(通知等待线程)
    "return 1; " +
"end; " +
"return nil;",
Arrays.asList(getName(), getChannelName()),  // KEYS[1]: 锁名称;KEYS[2]: 发布订阅通道
LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId)  // ARGV[3]: 线程标识
3. 关键点
  • 安全释放:仅锁持有者(UUID:threadId匹配)可释放锁。
  • 发布订阅:锁释放时通过Redis的PUBLISH通知等待线程。
  • 重入处理:通过hincrby -1递减重入次数,确保正确释放。

三、锁续时(看门狗机制)

1. 触发条件
  • 当使用无参lock()方法时(即未指定leaseTime),默认启用看门狗。
  • 看门狗默认每10秒(internalLockLeaseTime / 3)续期一次,将锁过期时间重置为30秒。
2. 核心源码
// RedissonLock.scheduleExpirationRenewal()
private void scheduleExpirationRenewal(long threadId) {
    ExpirationEntry entry = new ExpirationEntry();
    ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
    entry.addThreadId(threadId);
    
    // 创建定时任务
    Timeout task = commandExecutor.getConnectionManager().newTimeout(timeout -> {
        RFuture<Boolean> future = renewExpirationAsync(threadId);
        future.whenComplete((res, e) -> {
            if (e != null) {
                log.error("Can't update lock " + getName() + " expiration", e);
                return;
            }
            if (res) {
                // 续期成功,递归调用
                scheduleExpirationRenewal(threadId);
            }
        });
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
    
    entry.setTimeout(task);
}
3. 续期Lua脚本
// RedissonLock.renewExpirationAsync()
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +  // 锁存在且为当前线程持有
    "redis.call('pexpire', KEYS[1], ARGV[1]); " +  // 重置过期时间
    "return 1; " +
"end; " +
"return 0;",
Collections.singletonList(getName()),
internalLockLeaseTime, getLockName(threadId)
4. 关键点
  • 自动续期:通过Netty的Timeout实现定时任务。
  • 避免死锁:若业务执行时间超长,看门狗会持续续期,直到业务完成或线程崩溃。

四、重入锁实现

1. 数据结构

使用Redis的Hash存储锁信息:

  • Key:锁名称(如"resource")。
  • Field:线程标识(UUID:threadId)。
  • Value:重入次数(初始为1,每次重入+1)。
2. 加锁时的重入逻辑
// Lua脚本片段
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +  // 锁已存在,判断是否重入
    "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +  // 重入次数+1
    "redis.call('pexpire', KEYS[1], ARGV[1]); " +  // 重置过期时间
    "return nil; " +  // 返回nil表示加锁成功(重入)
"end; "
3. 解锁时的重入逻辑
// Lua脚本片段
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +  // 重入次数-1
"if (counter > 0) then " +  // 重入次数>0,继续持有锁
    "redis.call('pexpire', KEYS[1], ARGV[2]); " +  // 重置过期时间
    "return 0; " +  // 返回0表示锁未释放
"else " +  // 重入次数=0,释放锁
    "redis.call('del', KEYS[1]); " +  // 删除锁
    "return 1; " +  // 返回1表示锁已释放
"end; "
4. 关键点
  • 线程安全:通过UUID:threadId确保同一线程可重入。
  • 原子计数:使用hincrby保证计数操作的原子性。

五、lock()与tryLock()的区别

1. 核心区别对比表

特性

lock()

tryLock()

阻塞行为

阻塞直到获取锁

立即返回或在指定时间内等待

超时机制

无超时,默认启用看门狗自动续期

可自定义等待时间和锁持有时间

异常处理

不响应中断(抛出 InterruptedException

可响应中断(通过重载方法)

返回值

void

boolean(是否获取锁成功)

看门狗默认启用

是(无参时)

否(需显式设置超时参数)

典型场景

必须获取锁才能执行的场景

可重试或放弃的场景

2. 源码差异分析
// lock() 源码片段
public void lock() {
    try {
        lock(-1, null, false); // leaseTime=-1表示启用看门狗
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}

// tryLock() 源码片段
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
    // 1. 计算超时时间
    long time = unit.toMillis(waitTime);
    long current = System.currentTimeMillis();
    long threadId = Thread.currentThread().getId();
    
    // 2. 尝试获取锁
    Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
    if (ttl == null) {
        return true; // 获取成功
    }
    
    // 3. 超时处理逻辑(循环尝试或等待通知)
    // ...
}
3. 使用场景对比
// lock() 使用示例
RLock lock = redisson.getLock("order:123");
try {
    lock.lock(); // 阻塞直到获取锁
    // 执行关键业务逻辑
} finally {
    lock.unlock();
}

// tryLock() 使用示例
RLock lock = redisson.getLock("inventory:apple");
try {
    // 尝试在5秒内获取锁,持有30秒
    if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
        // 获取锁成功,执行操作
    } else {
        // 获取锁失败,执行降级逻辑
    }
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
} finally {
    if (lock.isHeldByCurrentThread()) {
        lock.unlock();
    }
}

六、总结

Redisson分布式锁的核心优势:

  1. 原子性:通过Lua脚本确保操作的原子性。
  2. 可重入:基于Hash结构实现线程级别的重入计数。
  3. 高可用:通过看门狗机制避免锁过期导致的数据不一致。
  4. 高性能:基于Netty的异步通信模型。
  5. 安全释放:通过UUID:threadId确保锁只能被持有者释放。

最佳实践建议

  • 优先使用 tryLock():避免长时间阻塞,提高系统吞吐量。
  • 明确锁持有时间:根据业务场景合理设置leaseTime,避免过度依赖看门狗。
  • 异常处理:使用带超时参数的tryLock(),并处理中断异常。

网站公告

今日签到

点亮在社区的每一天
去签到