Redisson可重入锁

144 阅读2分钟

原始redis分布式锁的问题

下面代码中,我们的trylock是通过setnx来实现,redis中存储的类型是string。

当第一个方法获取锁成功时,调用method2,由于还是在同一个线程内,方法1的锁还未释放,method2获取锁失败,无法重入。

Demo

    @Resource
    private RedissonClient redissonClient;

    private RLock lock;

    @BeforeEach
    void setUp() {
        lock = redissonClient.getLock("test");
    }

    @Test
    void testReentry() {
        boolean isLock = lock.tryLock();
        if (!isLock) {
            log.error("获取锁失败......1");
            return;
        }
        try {
            log.info("获取锁成功......1");
            method2();
            log.info("开始执行业务......1");
        } finally {
            log.info("准备释放锁......1");
            lock.unlock();
        }
    }

    private void method2() {
        boolean isLock = lock.tryLock();
        if (!isLock) {
            log.error("获取锁失败......2");
            return;
        }
        try {
            log.info("获取锁成功......2");
            log.info("开始执行业务......2");
        } finally {
            log.info("准备释放锁......2");
            lock.unlock();
        }
    }

Redisson的改进

redisson改进了锁的实现,不再使用string类型存储,而是hash类型。

key存储锁的标识,value中field存储当前线程标识,value存储重入次数。

当method2获取锁失败时,会继续判断是否是同一个线程,如果是的话,重入次数加一,获取锁成功,这样就达到了锁的重入。

释放锁时,不会直接删除锁。

method2先让重入次数减一,然后判断重入次数是否为0。等到重入次数为0时,再执行删除锁的逻辑.。

当然,上述的逻辑肯定不能通过java代码来实现,因为不能每一步操作结束后是否会因为一些未知原因阻塞。故是通过lua脚本来保证对redis的操作是原子性的。下面是lua脚本的实例:

获取锁

local key = KEYS[1]; -- 锁的key
local threadId = ARGV[1]; -- 线程的唯一标识
local releaseTime = ARGV[2]; -- 锁的自动释放时间
-- 判断是否存在
if(redis.call('exists', key) == 0) then
  -- 不存在,直接获取锁
  redis.call('hset', key, threadId, '1');
  -- 设置有效期
  redis.call('expire', key, releaseTime);
  return 1;
end;
--锁已经存在,判断threadId是否是自己
if(redis.call('hexists', key, threadId) == 1) then
  -- 是同一线程,获取锁,重入次数加1
  redis.call('hincrby', key, threadId, '1');
  -- 刷新有效期
  redis.call('expire', key, releaseTime);
  return 1;
end;
return 0; --锁不是当前线程的,获取锁失败

释放锁

local key = KEYS[1]; -- 锁的key
local threadId = ARGV[1]; -- 线程的唯一标识
local releaseTime = ARGV[2]; -- 锁的自动释放时间
-- 判断当前锁是否还是被当前线程自己持有
if(redis.call('hexists', key, threadId) == 0) then
  return nil; --如果不是自己,则直接返回
end;
-- 是自己的锁,则重入次数-1
local count = redis.call("hincrby", key, threadId, -1);
-- 判断重入次数是否已经为0
if(count > 0) then
  -- 大于0说明锁还不能释放,重置有效期然后返回
  redis.call('expire', key, releaseTime);
  return nil;
else
  -- 等于0说明需要释放锁,直接删除
  redis.call('del', key);
  return nil;
end;

我们翻看redisson源码,其实也是一样的。

image-20230512103955156.png

image-20230512104102573.png