被面了三次的 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存不存在,KEYS2 这里代表发布订阅消息的管道名称; 第二个if:判断KEY1的ARGV3是否存在,APGV3 代表线程id标识,如果不存在则返回null; 第三方if:如果存在将APGV3的值减一,如果counter的值相当于线程id标识经过运算后的值大于0,由于是可重入锁,该线程依旧可以获取到锁,重新设置ARGV2 锁过期时间,返回0; del操作是真正的解锁操作,并且通过publish命令发布消息,APGV1 代表消息内容,操作成功后返回1; 解锁失败返回nil,nil相当于Java中的null;

redis.call('set', KEYS3, 1, 'px', ARGV5);是为啥呢?解决这个问题哈,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,稍有不慎会生成大量线程,造成内存溢出。这种锁只限定简单场景(处理时间无法预估)使用。高并发场景我们能预估出时间,普通的锁就可以,设置超时长点就行。

相关推荐
谭光志31 分钟前
工具塞满上下文窗口怎么办?深度拆解 AI Agent Tool Search 按需加载实现原理
前端·后端·ai编程
她说..35 分钟前
Java 默认值设置方式
java·开发语言·后端·springboot
foggyprojects40 分钟前
从0开始,一句话启动AI DataAgent
后端·数据分析·ai编程
郡杰41 分钟前
一些基础和问题解决
后端
陈随易1 小时前
前端项目部署只要30秒
前端·后端·程序员
YIAN1 小时前
从零手写文件读取 MCP 服务:一文吃透 Model Context Protocol 全链路通信原理
前端·后端·mcp
Imchendiana1 小时前
《狂人日记NO.9》— 前后端一把梭,我的全栈实录
前端·后端
学渣超1 小时前
记一次分布式事务数据不一致的排查之旅:从超时到索引,层层剥茧
java·后端·架构
北冥you鱼2 小时前
Go 语言读取链上数据:从基础到实战
开发语言·后端·golang
程序员cxuan2 小时前
Claude Code 为了封禁中国用户,竟然在代码里下毒
人工智能·后端·程序员