被面了三次的 Redission Watchdog 原理,这次必须理清楚

前言

hi,大家好,我是大鱼七成饱(公众号同名)。

上周同事请教定时任务的方案,任务不能重复,时间不定,量不大,考虑了下,推荐Redission的Lock(带Watchdog机制那个)。忽然想起来,之前面试的时候也问到过,起码有三次关于 Redission WatchDog。感觉很重要,这次就梳理下,把原理总结清楚,保证下次问到,轻松吊打面试官。

首先是环境准备

xml 复制代码
<dependencies>
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.24.3</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <!-- 引入log4j2依赖 -->
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.26</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

Redission版本是3.24.3,集成到Springboot 3.1.6 版本。

举个例子

为方便理解,先举个简单例子,保存订单,给这个操作加锁,示例代码如下

less 复制代码
@PostMapping("addOrder")
    public ResponseEntity<Object> addGoods(@RequestBody Order order) {
        RLock rLock = redissonClient.getLock("order-lock" + order.getId());
        boolean islock = false;
        try {
            islock = rLock.tryLock(10, TimeUnit.SECONDS);
            if (!islock) {
                return ResponseEntity.ok("请稍后再试。。。");
            }
            orderService.addOrder(order);

            log.info("\n{} --> 获取到锁的睡眠40s", Thread.currentThread().getName());
            Thread.sleep(40000);
            log.info("\n{} --> 获取到锁的醒了", Thread.currentThread().getName());

            return ResponseEntity.ok(order);

        } catch (Exception e) {
            log.info("\n{} --> 获取锁失败:{}", Thread.currentThread().getName(), e);
        } finally {
            if (islock && rLock.isHeldByCurrentThread()) {
                rLock.unlock();
            }
        }
        log.info("\n{} --> 获取锁失败", Thread.currentThread().getName());
        return ResponseEntity.ok("服务暂时无法加载。。。");
    }

redissonClient.getLock就获取了一个带有Watch Dog 自动延时的锁,是不是特简单(有时候挺佩服大师的API设计)。

添加订单前,先根据订单id加锁,然后处理超时后,watch dog会自动将锁延时。处理完成后unlock下,释放锁。

原理图

会用了,那么执行原理是啥呢?有道是一 图 胜 千 言méi tú shūo ge dàn,下面上个图

有两个点需要注意:

  • watchDog 只有在未显示指定加锁超时时间(leaseTime)时才会生效。
  • lockWatchdogTimeout 设定的时间不要太小 ,比如设置的是 100毫秒,由于网络直接导致加锁完后,watchdog去延期时,这个key在redis中已经被删除了。

到这里了,我们看看源码

上锁

跟切西瓜一样,这刀先切哪里呢?既然 islock = rLock.tryLock(10, TimeUnit.SECONDS);这句上锁的,那就从这往下找,会进入如下代码,这里是上锁:

续期

兄弟们,接着往下走啊,继续找看门狗,如下图。

scheduleExpirationRenewal,看名字是不是特别亲切,定时过期和续租,这个方法继续debug,找到这个方法:renewExpiration,顾名思义,更新过期时间。里面有个TimerTask,这就是我们要找的看门狗(到期自动续租的定时任务)

renewExpiration调用的renewExpirationAsync实现如下:

typescript 复制代码
 protected CompletionStage<Boolean> renewExpirationAsync(long threadId) {
        return evalWriteAsync(getRawName(), 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(getRawName()),
                internalLockLeaseTime, getLockName(threadId));
    }

这就是续期的核心代码,使用lua执行的。getRawName是锁的名字(示例里的"order-lock" + order.getId())"),getLockName是线程+ UUID.randomUUID()拼接的,internalLockLeaseTime是续期的时间,如果key存在,则调用pexpire续期。这段代码就是源头活水,嘿嘿。

续期根源代码找到了,还要看看水流向哪里呀(怎么解锁的)。来都来了是吧,在继续看看

解锁

跟刚才一样,跟着unlock代码, unlock ->unlockAsync->unlockAsync0-> unlockInnerAsync

ini 复制代码
protected RFuture<Boolean> unlockInnerAsync(long threadId, String requestId, int timeout) {
        return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            "local val = redis.call('get', KEYS[3]); " +
            "if val ~= false then " +
            "return tonumber(val);" +
            "end; " +

            "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
                "return nil;" +
            "end; " +
             "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
              "if (counter > 0) then " +
                                        "redis.call('pexpire', KEYS[1], ARGV[2]); " +
                                        "redis.call('set', KEYS[3], 0, 'px', ARGV[5]); " +
                                        "return 0; " +
                                    "else " +
                                        "redis.call('del', KEYS[1]); " +
                                        "redis.call(ARGV[4], KEYS[2], ARGV[1]); " +
                                        "redis.call('set', KEYS[3], 1, 'px', ARGV[5]); " +
                                        "return 1; " +
                                    "end; ",
                                Arrays.asList(getRawName(), getChannelName(), getUnlockLatchName(requestId)),
                                LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime,
                                getLockName(threadId), getSubscribeService().getPublishCommand(), timeout);
    }

这里简单解释下执行流程

第一个if:判断加锁key存不存在,KEYS[2] 这里代表发布订阅消息的管道名称; 第二个if:判断KEY[1]的ARGV[3]是否存在,APGV[3] 代表线程id标识,如果不存在则返回null; 第三方if:如果存在将APGV[3]的值减一,如果counter的值相当于线程id标识经过运算后的值大于0,由于是可重入锁,该线程依旧可以获取到锁,重新设置ARGV[2] 锁过期时间,返回0; del操作是真正的解锁操作,并且通过publish命令发布消息,APGV[1] 代表消息内容,操作成功后返回1; 解锁失败返回nil,nil相当于Java中的null;

redis.call('set', KEYS[3], 1, 'px', ARGV[5]);是为啥呢?解决这个问题哈,github.com/redisson/re... attempt to unlock lock, not locked by current thread by node id。bug是16号提的,作者27号修的,感觉是临时修补。因为执行完后,有个方法删了这个锁,

ce.writeAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.DEL, getUnlockLatchName(id));为了不报错而已

一点思考

Watchdog每个锁都开启单独的Monitor,稍有不慎会生成大量线程,造成内存溢出。这种锁只限定简单场景(处理时间无法预估)使用。高并发场景我们能预估出时间,普通的锁就可以,设置超时长点就行。

相关推荐
水月梦镜花37 分钟前
redis:list列表命令和内部编码
数据库·redis·list
小码编匠1 小时前
一款 C# 编写的神经网络计算图框架
后端·神经网络·c#
AskHarries1 小时前
Java字节码增强库ByteBuddy
java·后端
佳佳_1 小时前
Spring Boot 应用启动时打印配置类信息
spring boot·后端
掘金-我是哪吒2 小时前
微服务mysql,redis,elasticsearch, kibana,cassandra,mongodb, kafka
redis·mysql·mongodb·elasticsearch·微服务
许野平3 小时前
Rust: 利用 chrono 库实现日期和字符串互相转换
开发语言·后端·rust·字符串·转换·日期·chrono
BiteCode_咬一口代码4 小时前
信息泄露!默认密码的危害,记一次网络安全研究
后端
ketil274 小时前
Ubuntu 安装 redis
redis
齐 飞4 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
LunarCod4 小时前
WorkFlow源码剖析——Communicator之TCPServer(中)
后端·workflow·c/c++·网络框架·源码剖析·高性能高并发