【Redisson】可重入锁剖析(一)

177 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第19天,点击查看活动详情

一、前言

RedissonRedis最知名的一个开源客户端框架,学习其:官方文档

  • 如何实现 Redis 分布式锁?
  • 各种分布式锁的原理

代码栗子如下:

// 尝试获取 myLock 这个key的锁
RLock lock = redisson.getLock("myLock");
​
// 常见加锁方式一:
// - 如果没有人加这把锁,那自己就可以加这把锁
// - 如果有人加这把锁了,那自己这时就会阻塞住
lock.lock();
​
// 常见加锁方式二:
// 尝试加锁,加锁成功后,10秒后自动释放锁
lock.lock(10, TimeUnit.SECONDS);
​
​
// 常见加锁方式三:
// 尝试加锁指定一个时间,最多等待 100秒:加锁成功返回 true,加锁失败返回 false
// 加锁成功后,10秒后自动释放锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
   try {
     ...
   } finally {
       // 释放锁 
       lock.unlock();
   }
}

Tips: 看门狗(watchdog)

为避免获取锁的 Redisson 实例客户端崩溃,导致锁无法释放。 看门狗,它会在锁持有者 Redisson 实例处于活动状态时延长锁的过期时间。默认锁定看门狗超时为 30 秒

上面是同步锁,现在看下异步锁:

RLock lock = redisson.getLock("myLock");
​
// 用其他的线程去进行加锁,不会阻塞当前的主线程运行
RFuture<Void> lockFuture = lock.lockAsync();
RFuture<Void> lockFuture = lock.lockAsync(10, TimeUnit.SECONDS);
RFuture<Boolean> lockFuture = lock.tryLockAsync(100, 10, TimeUnit.SECONDS);
lockFuture.whenComplete((res, exception) -> {
    // ...
    lock.unlockAsync();
});

Tips:用哪个线程去加分布式锁,就必须用那个线程来对分布式锁进行释放。

如果用不同的线程,会导致 IllegalStateException 异常。


二、可重入锁源码剖析

(1)可重入锁源码剖析之 lua 脚本

  1. 获取锁实例,从这个入手:
RLock lock = redisson.getLock("myLock");

进入 getLock 方法:

// Redisson.java
@Override
public RLock getLock(String name) {
​
    // commandExecutor 命令执行器
    // 封装了跟 redis 进行通信的 Connection 连接对象
    return new RedissonLock(commandExecutor, name, id);
}
  1. 加锁
lock.lock();

进入 lock() 方法:

// RedissonLock.java
@Override
public void lock(long leaseTime, TimeUnit unit) {
    try {
        lockInterruptibly(leaseTime, unit);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}
​
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, 
                                 long threadId, RedisStrictCommand<T> command) {
    internalLockLeaseTime = unit.toMillis(leaseTime);
​
    // 针对 redis 执行 lua脚本
    return commandExecutor
        .evalWriteAsync(getName(), LongCodec.INSTANCE, command,
             "if (redis.call('exists', KEYS[1]) == 0) then " +  // 是否存在对应的 key
             "redis.call('hset', KEYS[1], ARGV[2], 1); " +      // 设置 key 的值
             "redis.call('pexpire', KEYS[1], ARGV[1]); " +      // 设置 key 过期时间
             "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]);", // 返回当前 key 有效的存活期
         // 分别对应: KEYS[1]、ARGV[1]、ARGV[2]
         Collections.<Object>singletonList(getName()),
                        internalLockLeaseTime, getLockName(threadId));
}

用一张图来解释:

2020-09-0615:16.png

  1. Redis Cluster 读写操作时,会基于 key 计算 slot,得到哪台 master 机子
private NodeSource getNodeSource(String key) {
    // 集群模式下:CRC16.crc16(key.getBytes()) % 16384
    int slot = connectionManager.calcSlot(key);
    
    // 计算 master 实例:slot 属于哪个 master 实例
    MasterSlaveEntry entry = connectionManager.getEntry(slot);
    return new NodeSource(entry, slot);
}
  1. 执行 lua 脚本
  2. 启动看门狗(watchdog

Redisson 配置如下:

Config config = new Config();
        config.useClusterServers()
                .addNodeAddress("redis://172.18.1.23:7003")
                .addNodeAddress("redis://172.18.1.23:7004")
                .addNodeAddress("redis://172.18.1.24:7003")
                .addNodeAddress("redis://172.18.1.24:7004")
                .addNodeAddress("redis://172.18.1.26:7003")
                .addNodeAddress("redis://172.18.1.26:7004")
                .setPassword("123456");
​
RedissonClient redisson = Redisson.create(config);

(2)可重入锁源码剖析之看门狗维持加锁

看门狗(watchdog):在后台会不断去延长 key 对应的分布式锁的生存周期。

带着问题看源码:

  1. 分布式锁加锁成功后,watchdog 如何延长对应 key 的生存时间?
  2. 如果长期持有一把锁(不释放),锁对应的 key 的生存时间如何变化?
  3. 如果持有锁的机器宕机了,watchdog 如何处理?

1)分布式锁加锁成功后,watchdog 如何延长对应 key 的生存时间?

private Long tryAcquire(long leaseTime, TimeUnit unit) {
    return get(tryAcquireAsync(leaseTime, unit, Thread.currentThread().getId()));
}
​
private RFuture<Boolean> tryAcquireOnceAsync(long leaseTime,
                                             TimeUnit unit, final long threadId) {
    if (leaseTime != -1) {
        return tryLockInnerAsync(leaseTime, unit, threadId, 
                                 RedisCommands.EVAL_NULL_BOOLEAN);
    }
    // 执行 lua 脚本
    RFuture<Boolean> ttlRemainingFuture = 
        tryLockInnerAsync(LOCK_EXPIRATION_INTERVAL_SECONDS, TimeUnit.SECONDS, 
                          threadId, RedisCommands.EVAL_NULL_BOOLEAN);
​
    // 添加监听器
    ttlRemainingFuture.addListener(new FutureListener<Boolean>() {
        @Override
        public void operationComplete(Future<Boolean> future) throws Exception {
            if (!future.isSuccess()) {
                return;
            }
            // 对应 key 是否有存活时间
            Boolean ttlRemaining = future.getNow();
            // lock acquired
            if (ttlRemaining) {
                // 重点:调度过期续命
                scheduleExpirationRenewal(threadId);
            }
        }
    });
    return ttlRemainingFuture;
}

来详细看下 scheduleExpirationRenewal()

private void scheduleExpirationRenewal(final long threadId) {
    if (expirationRenewalMap.containsKey(getEntryName())) {
        return;
    }
​
    Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
        @Override
        public void run(Timeout timeout) throws Exception {
            // 这个 lua 脚本:更新过期时间
            RFuture<Boolean> future = commandExecutor.evalWriteAsync(
                getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +                                   "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                    "return 1; " +   // 设置成功,表明持有锁的客户端还在 
                "end; " +
                    "return 0;",     // 设置失败,表明持有锁的客户端不在
                    Collections.<Object>singletonList(getName()), 
                internalLockLeaseTime, getLockName(threadId));
​
            // 添加监听器
            future.addListener(new FutureListener<Boolean>() {
                @Override
                public void operationComplete(Future<Boolean> future) throws Exception {
                    // 执行成功,删除之前的调度
                    expirationRenewalMap.remove(getEntryName());
                    if (!future.isSuccess()) {
                        log.error("Can't update lock " + getName()
                                  + " expiration", future.cause());
                        return;
                    }
​
                    if (future.getNow()) {
                        // reschedule itself
                        // 这边递归又调用次
                        scheduleExpirationRenewal(threadId);
                    }
                }
            });
        }
        // internalLockLeaseTime 默认时间 30秒
        // 所以这每 10秒 就会调用一次
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
    
    // 取消
    if (expirationRenewalMap.putIfAbsent(getEntryName(), task) != null) {
        task.cancel();
    }
}

来看下,分布式锁存储在 Redis 是什么:

# Redis 中如下
172.18.1.23:7004> hgetall myLock
1) "dfd3aabb-82ce-4c54-966d-5719675a3d62:1"
2) "1"
​
# 实际上就类似 map:
# "dfd3aabb-82ce-4c54-966d-5719675a3d62:1",代表某客户端,1 是 threadId
# 1,代表自增次数
{
    "dfd3aabb-82ce-4c54-966d-5719675a3d62:1": 1
}

可以看到执行调度任务的默认时间是:10秒,internalLockLeaseTime / 3 = 10,默认 internalLockLeaseTime = 30秒

2)如果长期持有一把锁(不释放),锁对应的 key 的生存时间如何变化?

先在代码处打个断点:

2022-06-1607-37-30.png

再用 redis-cli 看锁对应的变化:

# 是否存在对应的 key
exists myLock
​
# 获取 myLock key的值
hgetall myLock
​
# 获取 myLock key的生存时间
pttl myLock

2022-06-1607-42-51.png

发现结果跟源码执行的逻辑一致:

  • 分布式锁对应的 key 的生存时间不断减少
  • 每次减少了 10秒,到剩余 20秒生存时间的时候,又被刷新为 30秒

3)如果持有锁的机器宕机了,watchdog 如何处理?

机子都宕机了,看门狗当然也没有。

对应的分布式锁就会在 30秒后过期。

其他客户端就可以在 30秒后获取到这把锁。