异常信息
ttempt to unlock lock, not locked by current thread by node id:*** thread-id: **
场景
有一个耗时可能很长的业务方法,做了异步处理@Async放入线程池执行。在controller来创建了锁,Rlock作为参数传到异步方法内,异步方法执行完finally内unlock.这个时候controller已经直接返回,异步方法可能执行了10秒钟后进入finally执行lock.unlock(),这时候报错上边的异常信息

贴代码:
controller层

package com.example.study.redisson;

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.TimeUnit;

/**
 * <h3>study</h3>
 * <p></p>
 *
 * @author : ZhangYuJie
 * @date : 2022-05-08 19:43
 **/
@Slf4j
@RestController
@RequestMapping("/test")
public class Controller {

    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private RedissonService redissonService;

    /**
     * 解锁异常情况模拟
     */
    @SneakyThrows
    @PutMapping("/redisson")
    public void TestRedisson(){
        RLock rlock = redissonClient.getLock("lock1");
        // 不设置锁时长,利用redis的看门狗默认30s,10s自动续期到30s,等待异步方法执行结束释放锁
        rlock.tryLock(5, TimeUnit.SECONDS);
        // 这里是异步方法
        redissonService.testRedisson(rlock);
        System.err.println("结束");
    }
}

service方法

package com.example.study.redisson;

import lombok.SneakyThrows;
import org.redisson.api.RLock;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

/**
 * <h3>study</h3>
 * <p></p>
 *
 * @author : ZhangYuJie
 * @date : 2022-05-08 19:46
 **/
@Service
public class RedissonService {

    @SneakyThrows
    @Async
    public void testRedisson(RLock lock) {
        try {
            // 任何业务代码
            System.err.println("testRedisson");
            Thread.sleep(100000);
        }finally {
        // sleep过后这一行代码unlock报错
            lock.unlock();
        }
    }
}

分析原因有可能是
A线程直接返回了,Rlock是在A线程创建,B线程虽然拿着锁但是在线程内执行逻辑耗时很长执行到finally释放锁的时候,但是这个时候锁已经不存在了。
那么问题来了,Rlock没有设置过期时间,按理来说redis的看门狗会自动续期。为什么锁直接消失了

经过查看redis的看门狗实现发现

分析原因有可能是:
A线程直接返回了,Rlock是在A线程创建,B线程虽然拿着锁但是在线程内执行逻辑耗时很长执行到finally释放锁的时候,但是这个时候锁已经不存在了。
那么问题来了,Rlock没有设置过期时间,按理来说redis的看门狗会自动续期。为什么锁直接消失了

经过查看redis的看门狗实现发现:

看门狗机制其实就是一个后台定时任务线程,获取锁成功之后,会将持有锁的线程放入到一个 RedissonLock.EXPIRATION_RENEWAL_MAP里面,然后每隔 10 秒 (internalLockLeaseTime / 3) 检查一下,如果客户端 还持有锁 key(判断客户端是否还持有 key,其实就是遍历 EXPIRATION_RENEWAL_MAP 里面线程 id 然后根据线程 id 去 Redis 中查,如果存在就会延长 key 的时间),那么就会不断的延长锁 key 的存活时间。
看到这里我们就知道为什么会看门狗失效了,因为我们A线程已经执行完结束,看门狗的看到我们的线程ID已经不存在了,自动动释放了锁,导致B线程unlock报错。
解决方案两种:
1、

	package com.example.study.redisson;

import lombok.SneakyThrows;
import org.redisson.api.RLock;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import java.util.Objects;

/**
 * <h3>study</h3>
 * <p></p>
 *
 * @author : ZhangYuJie
 * @date : 2022-05-08 19:46
 **/
@Service
public class RedissonService {

    @SneakyThrows
    @Async
    public void testRedisson(RLock lock) {
        try {
            // 任何业务代码
            System.err.println("testRedisson");
            Thread.sleep(100000);
        } finally {
            //如果锁被当前线程持有,则释放。多一个判断,减少报错信息的出现
            //在解锁之前先判断要解锁的key是否已被锁定并且是否被当前线程保持。如果满足条件时才解锁。
            if (Objects.nonNull(lock) && lock.isLocked() && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }

        }
    }
}

2、
在service的异步方法内加锁而不是在controller传入Rlock。

package com.example.study.redisson;

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.TimeUnit;

/**
 * <h3>study</h3>
 * <p></p>
 *
 * @author : ZhangYuJie
 * @date : 2022-05-08 19:43
 **/
@Slf4j
@RestController
@RequestMapping("/test")
public class Controller {

    @Autowired
    private RedissonService redissonService;

    @Autowired
    private RedissonClient redissonClient;
    /**
     * 解锁异常情况模拟
     */
    @SneakyThrows
    @PutMapping("/redisson")
    public void TestRedisson(){
        if(redissonClient.getBucket("test:1").isExists()){
            throw new Exception("线程在执行过程中,请稍后再试");
        };
        // 这里是异步方法
        redissonService.testRedisson();
        System.err.println("结束");
    }
}

package com.example.study.redisson;

import lombok.SneakyThrows;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

/**
 * <h3>study</h3>
 * <p></p>
 *
 * @author : ZhangYuJie
 * @date : 2022-05-08 19:46
 **/
@Service
public class RedissonService {

    @Autowired
    private RedissonClient redissonClient;

    @SneakyThrows
    @Async
    public void testRedisson() {
        RLock lock = redissonClient.getLock("test:1");
        // 不设置锁时长(tryLock如果设置锁时长看门狗会失效),利用redis的看门狗默认30s,10s自动续期到30s,等待异步方法执行结束释放锁
        lock.tryLock(5, TimeUnit.SECONDS);
        try {
            // 任何业务代码
            System.err.println("testRedisson");
            Thread.sleep(100000);
        } finally {
            lock.unlock();
        }
    }
}

另外还遇到一种可能导致看门狗失效:

如果本地调试过程中方法断点了很长时间,会认为服务宕机了,看门狗机制线程也就没有了,也不会延长 key 的过期时间,到了 30s 之后就会自动过期了,其他线程就可以获取到锁

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐