Redis怎样实现可重入分布式锁?

165 阅读3分钟

u=1724509320,3686655656&fm=253&fmt=auto&app=138&f=JPEG.webp
本文已参与「新人创作礼」活动,一起开启掘金创作之路。

前言

       在上文《Redis怎样实现分布式锁?》中,我们了解到了为什么需要分布式锁,以及怎样用Redis实现分布式锁。但是后来发现,用这篇文章中的方式来实现,会有一个弊端,即这个分布式锁无法重入。本文将为大家讲解其解决办法。

Redis分布式锁弊端原理

       在《Redis怎样实现分布式锁?》中,我们是通过Redis的set命令,再加上其NX参数来实现分布式锁的。而设置NX参数后,如果Redis中已存在key(即锁),则无法写入成功,也就无法成功加锁。因此,如果一个线程已经获取到一个锁A以后,在未释放锁之前,即便是其本身,也无法再获取到相同的锁A,即无法重入。

可重入锁解决办法

       从上述可知,我们没办法在Redis层级去实现可重入,因此,我们需要再引入一个组件,用来记录当前线程获取到的锁及其次数,从而让我们在加锁的时候,可以校验当前线程已获取过多少次锁。而与多线程相关,且各自维护数据的,我们首先想到的就是ThreadLocal

ThreadLocal会为每一个线程都提供一份变量的副本,从而实现同时访问而互不相干扰。

可重入锁实现逻辑

       我们先为分布式锁定义一个ThreadLocal,代码如下:

/**线程本地存储,存储锁的密钥和计数:结构为 锁id:加锁计数*/
private static ThreadLocal<Map<String, Integer>> threadLocal = new ThreadLocal<>();

加锁

    /**
     * 尝试获取锁
     */
    @Override
    public boolean tryLock() {
        // 当前线程持有的锁及其计数
        Map<String,Integer> value = threadLocal.get();
        if(value.containsKey(lockKey)){
            value.put(lockKey,value.get(lockKey) + 1);
            return true;
        }

        // 获取锁
        String result = jedisCluster.set(lockKey, requestId, "NX", "EX", 2);
        // 获取锁成功
        if (StringUtils.isNotBlank(result) && LOCK_SUCCESS.equals(result)) {
            value.put(lockKey,1);
            return true;
        }
        return false;
    }

       如上述代码,在获取锁的时候,先确认当前线程是否已持有锁,有的话,增加锁计数;没有的话,就调用Redis实现加锁。加锁成功后,将lockKey写入到ThreadLocal中,表示当前线程已持有该锁。

释放锁

    /**
     * 释放锁
     */
    @Override
    public void unlock(String lockKey,String requestId) {
        // 当前线程持有的锁及其计数
        Map<String,Integer> value = threadLocal.get();
        if(value.containsKey(lockKey)){
            Integer num = value.get(lockKey) - 1;
            // 锁计数小于等于0,需要调用Redis进行锁释放
            if(num <= 0){
                // 若redis中存在lockKey,则删除lockKey,否则返回0
                String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
                Object result = jedisCluster.eval(script,Collections.singletonList(lockKey),Collections.singletonList(requestId));
                // 成功
                if (result != null && "OK".equals(result.toString())) {
                    logger.debug("释放锁成功,lockKey:{}, requestId:{}", lockKey, requestId);
                    value.remove(lockKey);
                } else {// 失败
                    logger.debug("释放锁失败,lockKey:{}, requestId:{}", lockKey, requestId);
                }
            } else {
                // 减少锁计数
                value.put(lockKey,num);
            }
        } else {
            logger.debug("释放锁成功,当前线程未持有锁,lockKey:{}, requestId:{}", lockKey, requestId);
        }
    }

       如上述代码,在释放锁的时候,先确认当前线程是否已持有锁,有的话,减少锁计数;没有的话,直接释放成功。如果锁计数小于等于0,则需要调用Redis释放锁,释放成功后,将lockKeyThreadLocal中删除,表示当前线程已不再持有该锁。

后言

       既然看到这里了,感觉有所收获的朋友,不妨来个大大的点赞吧~~~