Redis分布式锁使用,含Redisson源码分析

Redis分布式锁使用,含Redisson源码分析

  • 本文不收取任何什么费用,如果有显示什么VIP之类的,估计就是平台看我太久没上线,强行收费了

Redis分布式锁

  • 在程序员自己简单的使用set key value nx 超时时间来实现锁,这个key 一般是所需要锁住的业务,如order:某个商品序号,它的value会设计为一个UUID + 线程号的方式

分布式锁Key的设置

  • 为什么会这样设置key,因为key是为了区别不同业务下不同的锁,如果一个用户抢手机商品,一个用户抢优惠卷,他们两个肯定不是一把锁,所以需要将它们区分,区分的方式就采用key来区别,手机的Keylock:phone:商品ID,优惠卷的Keylock:coupon:优惠卷ID

分布式锁Value的设置

  • 为什么这样设置value,试想一个场景,当一个线程正常获取了锁,但是出于某种原因,他被阻塞了(阻塞的原因可能是GC垃圾回收之类的原因),在阻塞的这段时间,它自己获得的锁超时了
  • 在超时过后,另外的线程set nx的时候发现没有锁了,这个时候让新来的线程获取到了锁
  • 但是这个时候,原来阻塞的线程从阻塞状态中恢复到了运行状态,在它正常执行完业务逻辑代码后,准备要释放锁,但是这个时候,它自己的锁其实是超时删除了的,Redis中并没有它对应的锁,有的是一个新来的线程的锁,那么它在释放锁的时候是直接将别人的锁释放掉了,这样,别的线程都能够进入对应的业务逻辑,就可能产生线程安全问题

出现以上场景的原因就是:在释放锁的时候,并没有检查这把锁是不是自己的就直接释放了,所以可能释放到了别的线程的锁,所以要在value处设置是哪个线程的锁,添加上UUID是因为保证在分布式的场景下的安全

实现

public class SimpleRedisLock{

    // 锁业务key
    private String name;

    private StringRedisTemplate redisTemplate;

    // 锁业务key前缀
    private static final String KEY_PREFIX = "lock:";

    // 锁业务value前缀
    private static final String ID_PREFIX = UUID.randomUUID() + "-";
    
    public SimpleRedisLock(String name, StringRedisTemplate redisTemplate) {
        this.name = name;
        this.redisTemplate = redisTemplate;
    }

    /**
     * 锁业务
     * @param leaseTime 超时时间
     * @param unit 时间单位
     * @return 是否获取锁成功,成功返回true,失败返回false
     */
    public boolean tryLock(int leaseTime, TimeUnit unit){
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        Boolean success = redisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, leaseTime, unit);
        return Boolean.TRUE.equals(success);
    }

    /**
     * 释放锁
     */
    public void unlock(){
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        String id = redisTemplate.opsForValue().get(KEY_PREFIX + name);
        if (threadId.equals(id)){
            redisTemplate.delete(KEY_PREFIX + name);
        }
    }
}

存在的问题

  • 以上自己写了一个分布式锁,在绝大多数环境下是够用了,但仍然存在问题
  • 如果一个线程,它是执行到了unlock()的时候并且判断完了if后,线程才被阻塞,也就是说,这个线程要是恢复了过来,它下一步就不再判断是不是自己的锁了,它直接就会对锁进行删除,这种场景出现的概率不大,但仍然是有可能的
  • 为什么会出现这种原因呢,因为释放锁的过程应该要是原子性的,也就是中间不能停下来执行别的程序,而通过Lua脚本能够完成这种功能

编写lua脚本

-- 不过多介绍语法,这里讲的是Redis
if(redis.call('get', 'key') == ARGV[1]) then
  return redis.call('del', KEYS[1])
end
return 0

改良后的实现

  • 讲脚本拷贝到一个resource资源文件中创建一个lua文件unlock()函数进行修改
public class SimpleRedisLock{

    // 锁业务key
    private String name;

    private StringRedisTemplate redisTemplate;

    // 锁业务key前缀
    private static final String KEY_PREFIX = "lock:";

    // 锁业务value前缀
    private static final String ID_PREFIX = UUID.randomUUID() + "-";

    private static DefaultRedisScript<Long> UNLOCK_SCRIPT;

    static {
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }

    public SimpleRedisLock(String name, StringRedisTemplate redisTemplate) {
        this.name = name;
        this.redisTemplate = redisTemplate;
    }

    /**
     * 锁业务
     * @param leaseTime 超时时间
     * @param unit 时间单位
     * @return 是否获取锁成功,成功返回true,失败返回false
     */
    public boolean tryLock(int leaseTime, TimeUnit unit){
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        Boolean success = redisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, leaseTime, unit);
        return Boolean.TRUE.equals(success);
    }

    /**
     * 释放锁
     */
    public void unlock(){
        redisTemplate.execute(UNLOCK_SCRIPT, Collections.singletonList(KEY_PREFIX + name), ID_PREFIX + Thread.currentThread().getId());
    }
}

可以改进的点

  • 上面的代码足够解决很多常见的问题了,但是还可以让这个锁更加的完善,可以有以下两点完善
    • 这个锁无法重入
      • 可重入锁(Reentrant Lock)是一种在同一个线程中可以多次获取的锁。它允许线程在已经持有锁的情况下再次获取该锁,而不会被自己锁住
    • 锁有可能因为阻塞导致锁释放,而不是因为业务执行完毕而释放
  • 根据这两个点,可以对上面的锁再次进行完善,但是在实际工作过程中,不推荐自己编写这样的锁,现在已经有了现成的,大牛们帮我们写的框架Redisson

Redisson

快速入门

  • 引入依赖
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.13.6</version>
</dependency>
  • 配置Redisson客户端,尽管Redisson确实有SpringBoot的starter场景启动器,但是如果直接导入starter的话,会导致Spring官方提供的redis配置失效(被覆盖),为了保留Spring的配置,单独引入Redisson,并且单独写配置文件
@Configuration
public class RedisConfig {

    @Bean
    public RedissonClient redissonClient {
        // 配置类
        Config config = new Config();
        config.useSingleServer().setAddress("redis://localhost:6379").setPassword("123456");
        return Redisson.create(config);
    }
}
  • 使用Redisson的分布式锁
    • tryLock()函数有三种重载方式分别是
      • 空参:不进行重试,并且锁30秒后会释放
      • 双参:表示重试时间和重试时间的单位(秒、毫秒)
      • 三参:表示重试时间和锁过期时间,时间单位
@Resource
private RedissonClient redissonClient;

public testRedisson() throws InterruptException {
    // 获取锁,指定锁的名字
    RLock lock = redissonClient.getLock("anyLock");
    // 尝试获取锁,参数为:获取锁的最大等待时间(期间会重试)、锁自动释放时间、时间单位
    // 如果设置无参数,表示leaseTime = -1,锁自动释放时间是30秒
    boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS);
    if(isLock) {
        try{
            System.out.println("执行业务");
        }finally{
            lock.unlock();
        }
    }
}

Redisson可重入锁原理

  • 在上面编写的Redis分布式锁是无法完成可重入的操作,因为上锁的逻辑是当reids中没有值的话,才在redis中设置一个key,如果有了这个值就等待,所以无法再次获得锁,也就没有办法重入
  • Redis重入的原理:
    • 在Redis中不使用String类型当作锁,使用的Hash类型当作锁,Hash的key是需要锁的业务名称,而域的键是UUID + 线程值,域的值是重入的次数
    • 锁的业务名字域的键不过多介绍,和之前实现的逻辑是一样的,重点介绍这个多出来的域的值
    • 业务逻辑转变为
      • 当一个线程获取锁,先判断是否有这个key,如果没有就新建一个Hash类型
      • 如果有这个key,那么判断它的field是不是为当前线程,如果是,value+1表示重入,允许进行加锁,如果需要释放锁,那么也需要判断field是不是本线程,如果是value-1,如果value=0表示能够删除key,如果不为0,则继续
    • image.png

Redisson锁重试机制原理

  • 跟踪boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS);中的tryLock(),会发现tryLock()只是接口所定义的一个接口,查找它的一个实现类RedissonLock,查看它的tryLock()函数,以下列出它的部分源码
public class RedissonLock{
    public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
        // 将重试时间转化为毫秒,并且赋值给time(记住这个time的值是重试时间)
        long time = unit.toMillis(waitTime);
        // 记录锁第一次进来的时间
        long current = System.currentTimeMillis();
        // 得到想获得锁的线程ID
        long threadId = Thread.currentThread().getId();
        // 这一步是重要的一步,这个函数根据它的名字能够看出只是尝试去获得,返回的应该是一个时间
        Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
    }
}
  • 根据刚进来的源码,执行到了tryAcquire()这个函数去获取锁,接下来分析这个函数做了什么
public class RedissonLock{
    private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
        // 根据这个函数名称发现它只是去尝试异步获取锁
        return get(tryAcquireAsync(waitTime, leaseTime, unit, threadId));
    }
}
  • 注意,这个函数还是在RedissonLock这个类里面,并没有出这个类,但是还是不知道它是怎么去锁的,所以接下来跟tryAcquireAsync()这个函数
public class RedissonLock{
    private RFuture<Boolean> tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
        // 我们在调用leaseTime的时候明显不为-1,所以进入if判断中
        if (leaseTime != -1) {
            return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
        }
        // 执行到这个表示lease肯定为-1了,不然的话,已经在上面return了,后面再来分析后面这一段
        RFuture<Boolean> ttlRemainingFuture = tryLockInnerAsync(waitTime,
                                                    commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
                                                    TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
        ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
            if (e != null) {
                return;
            }

            // lock acquired
            if (ttlRemaining) {
                scheduleExpirationRenewal(threadId);
            }
        });
        return ttlRemainingFuture;
    }
}
  • 这个函数仍然是在RedissonLock这个类里面,这里发现源码对leaseTime进行判断了,可能会觉得奇怪,为什么leaseTime会要进行判断呢?
    • 如果对上面的快速入门还有印象的话,就知道,在我们没有设置的时候默认leaseTime = -1,可以自行跟以下源码,最后都会调用到上面的函数
  • 因为我们这里自己设置了leaseTime,所以肯定是会走到第一个if后,直接返回,直接返回这一点很重要,因为如果我们没有设置的话,Redisson其实是会帮我们做一点事情的,如果直接在if返回,后续Redisson就无法帮我们做事,因为它已经return了
  • 我们先看if里面到底是做了什么后,再看Redisson帮我们做了什么,接下来跟tryLockInnerAsync()函数
public class RedissonLock{
    <T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        // 找了一个变量,存储了leaseTime的时间
        internalLockLeaseTime = unit.toMillis(leaseTime);

        // 执行lua脚本
        return evalWriteAsync(getName(), LongCodec.INSTANCE, command,
                // 需要记住的是,脚本里面的KEYS[]和ARGV[]的值,其实是跟在它后面的,和前面的无关
                // KEYS的参数在Collections.singletonList()里面
                // ARGV的参数是Collections.singletonList()后面的不定长参数

                // 通过redis判断的getName()其实是在我们
                // RLock lock = redissonClient.getLock("anyLock");这里的"anyLock"
                // 也就是说先在redis中判断这个key是否存在,如果为0表示不存在,1表示存在
                "if (redis.call('exists', KEYS[1]) == 0) then " +
                        // key不存在了,表示没人来获取过,所以肯定获取锁
                        "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                        // 再为key设置一个毫秒过期时间
                        "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                        // 成功获取锁,返回null
                        "return nil; " +
                        // if结束
                        "end; " +
                        // 下一个if,到这里表示这个key肯定存在了,也就是有线程锁住了,这个时候判断这个锁是不是自己锁的
                        // 判断获取锁的线程是不是这个要来获取的线程,也就是看看能不能重入
                        "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                        // 进来了if,表示想要重入,所以value+1
                        "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                        // 充值key的过期时间
                        "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                        // 成功获取锁,返回null
                        "return nil; " +
                        // if结束
                        "end; " +
                        // 到这里表示这个key存在,并且锁资源的不是它本身,返回的是别人还需要锁住这个key多久
                        "return redis.call('pttl', KEYS[1]);",
                Collections.singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
    }
}
  • 这个函数还是在RedissonLock类里面,这里需要注意的点其实是,它找了一个实例变量,存储了这个锁的过期时间,需要记住有这么一回事,存储后执行了lua脚本,为了保证获取锁的原子性,这里还需要记得,如果返回null表示获取锁成功,如果返回的是毫秒数,表示别人占用这个锁的时间,接下来返回到上面的函数
public class RedissonLock{
    private RFuture<Boolean> tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
        // 刚刚将这个if里面的东西判断完了,走的if里面
        if (leaseTime != -1) {
            return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
        }
        // 现在想看看如果没走if是什么样
        RFuture<Boolean> ttlRemainingFuture = tryLockInnerAsync(waitTime,
                                                    commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
                                                    TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
        ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
            if (e != null) {
                return;
            }

            // lock acquired
            if (ttlRemaining) {
                scheduleExpirationRenewal(threadId);
            }
        });
        return ttlRemainingFuture;
    }
}
  • 记住,上面执行完if里面的东西后,直接就返回了,没有下面什么事情,这里分析下面这一段是因为想看看如果是没有设置过期时间,Redisson会为我们做些什么而已
  • 可以看到它同样是调用了tryLockInnerAsync()这个函数,他和上面不同的点在于leaseTime的设置,Redisson为我们赋值的是:commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),剩下的其实都是一摸一样的,也就是返回值,如果是null表示成功获取到了锁,如果是毫秒数,表示获取锁失败,并且这个时间是别人还需要锁这个资源多久,记住,是别人还需要锁多久,那么Redisson赋值的是什么?
    • 这里不列出来了,直接告诉各位,就是30秒,也就是说,直到这一步,默认的-1,才设置成30秒,这个Redisson为我们设置的,是一个叫做看门狗的东西,设置完以后,后面这一段暂且不看,这一段是Redisson为我们设置的看门狗的功能
  • 从这里返回的是这里的代码,下面看看后面又做了些什么
public class RedissonLock{
    public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
        // 将重试时间转化为毫秒,并且赋值给time
        long time = unit.toMillis(waitTime);
        // 记录锁第一次进来的时间
        long current = System.currentTimeMillis();
        // 得到想获得锁的线程ID
        long threadId = Thread.currentThread().getId();
        // 这一步是重要的一步,这个函数根据它的名字能够看出只是尝试去获得,返回的应该是一个时间
        Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
    }
}
public class RedissonLock{
    public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
        long time = unit.toMillis(waitTime);
        long current = System.currentTimeMillis();
        long threadId = Thread.currentThread().getId();
        // 刚刚从这里返回来
        Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
        // 提到过,获取锁返回的null,如果是null,表示获取锁成功,返回true
        if (ttl == null) {
            return true;
        }
        // 这里表示没有获取锁成功
        // 计算上面获取这把锁所消耗的时间,并且用重试的时间 - 获取这把锁所消耗的时间
        // 减去后是剩下的重试时间
        time -= System.currentTimeMillis() - current;
        // 重试的时间 - 获取这把锁所消耗的时间 如果 < 0表示已经没有时间重试了,直接返回失败
        if (time <= 0) {
            acquireFailed(waitTime, unit, threadId);
            return false;
        }
        // 如果还有时间,再记录现在的时间
        current = System.currentTimeMillis();

        // 这里看到了一个subscribe,表示订阅,这个订阅是在锁释放后会发布的一个订阅
        // 后续会查看到代码
        RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
        // 在time时间,也就是重试的时间内,如果发现有线程发布了主题,才会往下继续执行
        // 如果没有发布对应的主题,表示这个线程无法再进行重试,进入if语句
        if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {
            if (!subscribeFuture.cancel(false)) {
                subscribeFuture.onComplete((res, e) -> {
                    if (e == null) {
                        // 取消订阅发布的主题
                        unsubscribe(subscribeFuture, threadId);
                    }
                });
            }
            acquireFailed(waitTime, unit, threadId);
            // 返回无法获得锁
            return false;
        }

        // 到这里表示收到对应的锁释放的主题
        try {
            // 这里重新记录剩下的重试时间
            time -= System.currentTimeMillis() - current;
            // 如果重试时间已经 < 0表示它没办法再去获取锁了
            if (time <= 0) {
                acquireFailed(waitTime, unit, threadId);
                return false;
            }
            // 这里是真正的去获得锁
            while (true) {
                long currentTime = System.currentTimeMillis();
                // 因为前面是收到了锁同一个资源的锁释放的消息
                // 这里才会重新尝试去获取锁
                // 到这里才算是第二次尝试获取
                ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
                // 如果是null表示获取到了
                // 不为null表示这个ttl是锁同一资源的key的剩余时间
                if (ttl == null) {
                    return true;
                }
                // 重新计算重试的剩余时间
                time -= System.currentTimeMillis() - currentTime;
                // 如果重试时间<0表示无法获得锁
                if (time <= 0) {
                    acquireFailed(waitTime, unit, threadId);
                    return false;
                }

                // 保存当前时间
                currentTime = System.currentTimeMillis();
                // 记住ttl是锁同一资源的key的剩余时间
                // 同一把锁的时间 > 0,并且同一把锁的时间 < 我剩下的重试时间走if里面
                if (ttl >= 0 && ttl < time) {
                    // 等待一个ttl的时间,也就是等锁一释放,执行定时任务,也就是抢夺锁
                    subscribeFuture.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                } else {
                    // 到这里只有一个可能,那就是锁释放的时间已经超过了重试的时间
                    // 那么我在别人还没有释放锁的时候,并且在我重试时间到了以后,最后尝试获取一次
                    // 获得的到的话就执行任务,获取不到就不执行
                    subscribeFuture.getNow().getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
                }
                // 最后计算这次获取锁所耗费的时间
                // 并且计算出剩余的重试时间
                time -= System.currentTimeMillis() - currentTime;
                // 重试时间 < 0表示已经无法获取锁了,退出函数
                if (time <= 0) {
                    acquireFailed(waitTime, unit, threadId);
                    return false;
                }
            }
        } finally {
            // 出现任何情况,都会取消掉订阅的消息
            unsubscribe(subscribeFuture, threadId);
        }
    }
}
  • 这就是Redisson的锁重试机制的原理,里面使用了发布订阅的方式,并且通过定时任务,让其不会一直无条件的死循环,降低了CPU的压力

Redisson看门狗机制原理

  • 根据上面的源码分析后,不知道是否还记得,上面还有一个点没有讲诉到,就是之前提到的Redisson在我们没有设置leaseTime的时候,leaseTime被赋值-1,根据-1判断后设置了30秒给leaseTime,后面的代码并没有进行分析,就直接跳过了,现在再来对其进行分析
public class RedissonLock{
    private RFuture<Boolean> tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
        // 我们在调用leaseTime的时候明显不为-1,所以进入if判断中
        if (leaseTime != -1) {
            return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
        }
        // 执行到这个表示lease肯定为-1了,不然的话,已经在上面return了,后面再来分析后面这一段
        RFuture<Boolean> ttlRemainingFuture = tryLockInnerAsync(waitTime,
                                                    commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
                                                    TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
        // 尝试获取完锁后,不管有没有获取到锁,都会走下面的逻辑
        ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
            if (e != null) {
                return;
            }

            if (ttlRemaining) {
                // 这里执行了一个超时续约
                scheduleExpirationRenewal(threadId);
            }
        });
        return ttlRemainingFuture;
    }
}
  • 在Redisson为我们设置的超时时间可以看到,不管是否能够获取到锁,它都会执行一个超时续约的函数,接下来主要分析这个函数具体干了什么
public class RedissonLock{
    private void scheduleExpirationRenewal(long threadId) {
        // 这里new了一个entry,entry就是和hashmap里面那个entry是一样的
        // 存储的是Map里面键值对的值
        ExpirationEntry entry = new ExpirationEntry();
        // 这里的EXPIRATION_RENEWAL_MAP其实是juc里面的ConcurrentHashMap
        // 具体功能不再说明,感兴趣可以自行查找资料
        // put进去的值是一个函数getEntryName()
        // 这个函数会返回一个entryName,这个实例变量的值是线程的id:new Redisson所传入的name
        // 这个的返回值,如果这个entryName不存在的话,也就是第一次进入这个函数,返回null
        // 如果存在,返回的就是之前的oldEntry,也就不止一次进入这个函数,也就不为null
        ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
        if (oldEntry != null) {
            // 进来这里表示之前之前来过
            // 回忆一下,进来这里表示之前已经来过一次了
            // 那这里是怎么进来的?
            // 能到这里表示之前就尝试锁了,这里是第二次想尝试获取了
            // 然后为entry中放入一个线程的ID
            oldEntry.addThreadId(threadId);
        } else {
            entry.addThreadId(threadId);
            // 如果是第一次来,那么不仅需要放一个线程ID
            // 还执行了一个超时续约,接下来对这个函数进行分析
            renewExpiration();
        }
    }
}
public class RedissonLock{
    private void renewExpiration() {
        // 这里通过ConcurrentHashMap获取线程对应的entry
        ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
        if (ee == null) {
            return;
        }

        // 这里开启一个异步线程,执行任务,先不看里面具体内容
        // 查看这个线程的后两个参数internalLockLeaseTime / 3, TimeUnit.MILLISECONDS
        // 表示这个异步任务,每经过三分之一的internalLockLeaseTime就会执行一次
        // 那么,是否记得这个internalLockLeaseTime是什么呢
        // 这个值就是在上锁的时候提到的
        Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
            @Override
            public void run(Timeout timeout) throws Exception {
                ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
                if (ent == null) {
                    return;
                }
                Long threadId = ent.getFirstThreadId();
                if (threadId == null) {
                    return;
                }
                
                RFuture<Boolean> future = renewExpirationAsync(threadId);
                future.onComplete((res, e) -> {
                    if (e != null) {
                        log.error("Can't update lock " + getName() + " expiration", e);
                        return;
                    }
                    
                    if (res) {
                        // reschedule itself
                        renewExpiration();
                    }
                });
            }
        }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
        
        ee.setTimeout(task);
    }
}
public class RedissonLock{
    <T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        
        
        // 就是这里,当初讲解的时候让记住的,如果忘记了,请再认真看一遍完整的流程
        internalLockLeaseTime = unit.toMillis(leaseTime);



        
        return evalWriteAsync(getName(), LongCodec.INSTANCE, command,
                "if (redis.call('exists', KEYS[1]) == 0) then " +
                        "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                        "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); " +
                        "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                        "return nil; " +
                        "end; " +
                        
                        "return redis.call('pttl', KEYS[1]);",
                Collections.singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
    }
}
public class RedissonLock{
    private void renewExpiration() {
        // 这里通过ConcurrentHashMap获取线程对应的entry
        ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
        if (ee == null) {
            return;
        }

        // 这里开启一个异步线程,执行任务,先不看里面具体内容
        // 查看这个线程的后两个参数internalLockLeaseTime / 3, TimeUnit.MILLISECONDS
        // 表示这个异步任务,每经过三分之一的internalLockLeaseTime就会执行一次
        // 那么,是否记得这个internalLockLeaseTime是什么呢
        // 这个值就是在上锁的时候提到的

        // 
        Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
            @Override
            public void run(Timeout timeout) throws Exception {
                ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
                if (ent == null) {
                    return;
                }
                Long threadId = ent.getFirstThreadId();
                if (threadId == null) {
                    return;
                }

                // 异步线程主要里面有一个这个方法,点进去查看一下
                RFuture<Boolean> future = renewExpirationAsync(threadId);
                future.onComplete((res, e) -> {
                    if (e != null) {
                        log.error("Can't update lock " + getName() + " expiration", e);
                        return;
                    }
                    
                    if (res) {
                        // reschedule itself
                        renewExpiration();
                    }
                });
            }
        }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
        
        ee.setTimeout(task);
    }
}
public class RedissonLock{
    protected RFuture<Boolean> renewExpirationAsync(long threadId) {
        return 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.singletonList(getName()),
                internalLockLeaseTime, getLockName(threadId));
    }
}
  • 直接说明这个Lua脚本的意思,表明对一个Redis中的Key,重置它的过期时间为internalLockLeaseTime
public class RedissonLock{
    private void renewExpiration() {
        // 这里通过ConcurrentHashMap获取线程对应的entry
        ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
        if (ee == null) {
            return;
        }

        // 这里开启一个异步线程,执行任务,先不看里面具体内容
        // 查看这个线程的后两个参数internalLockLeaseTime / 3, TimeUnit.MILLISECONDS
        // 表示这个异步任务,每经过三分之一的internalLockLeaseTime就会执行一次
        // 那么,是否记得这个internalLockLeaseTime是什么呢
        // 这个值就是在上锁的时候提到的

        // 
        Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
            @Override
            public void run(Timeout timeout) throws Exception {
                ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
                if (ent == null) {
                    return;
                }
                Long threadId = ent.getFirstThreadId();
                if (threadId == null) {
                    return;
                }

                // 异步线程主要里面有一个这个方法,点进去查看一下
                RFuture<Boolean> future = renewExpirationAsync(threadId);
                future.onComplete((res, e) -> {
                    if (e != null) {
                        log.error("Can't update lock " + getName() + " expiration", e);
                        return;
                    }
                    
                    if (res) {
                        // 递归执行自己本身
                        renewExpiration();
                    }
                });
            }
        }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
        
        ee.setTimeout(task);
    }
}
  • 看到了递归执行,但是其实不用慌,这个递归有前提的,就是根据Redisson为我们所设置的internalLockLeaseTime,也就是30秒,除以3,也就是10秒,这个递归才会开始执行一次,每一次的执行,都是会刷新这个锁的过期时间,让他回到30秒,这样做的好处是,一个线程如果释放了锁,不会是因为线程阻塞的原因导致的,只有可能是业务执行完毕

到这里,其实Redisson的看门狗机制就讲解完毕了,总结来就是,当第一次获取锁时,会开启一个异步任务,这个任务会不断的刷新锁的时候,防止因为线程阻塞的原因,导致锁释放,如果是第二次进入,则不会再开启这个刷新任务了

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值