子线程重入父线程的redisson分布式锁

场景:

        因项目业务需要,一个线程在抢到了redisson分布式锁后,开启子线程,子线程同样需要这把锁来处理一些业务逻辑。

前提:

        redisson分布式锁的原理这里不再赘述。redisson是通过“lua脚本+state计数”来实现重入锁的。redisson使用一个hash结构来存储锁,其中key表示该锁是否存在,field标识线程的持有者,value为锁的重入次数。

"if (redis.call('exists', KEYS[1]) == 0) then " +   -- 判断锁是否已经存在
  "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +  -- 如果锁不存在,创建一把新的锁,记录锁的持有者并设置重入次数为1
  "redis.call('pexpire', KEYS[1], ARGV[1]); " +     -- 设置锁的过期时间
  "return nil; " +
"end; " +  
-- 如果锁已经存在,则继续判断尝试加锁的线程是不是当前锁的持有者(重入)
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
  "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +  -- 锁的重入次数+1
  "redis.call('pexpire', KEYS[1], ARGV[1]); " +     -- 刷新锁的超时时间
  "return nil; " +
"end; " +
-- 尝试加锁的线程不是当前锁的持有者,直接返回锁的剩余时间
"return redis.call('pttl', KEYS[1]);"

lua脚本中的三个参数含义:

  • KEYS[1]:锁名称,即rlock.tryLock("锁名称")
  • ARGV[1]:锁的过期时间 默认为30秒
  • ARGV[2]:锁的field,值为 客户端id + “:” + 线程id

从这里看出,子线程想要重入父线程的锁,只需要同样的客户端id和线程id就可以实现。

解决思路:

        1、客户端id,子线程和父线程使用同一个锁对象即可

        2、线程id,父线程需要将锁的信息传递给子线程。可以通过将锁对象作为参数传递给子线程,或者通过共享的变量(如静态变量或者线程局部变量)来实现。这里推荐使用阿里的TransmittableThreadLocal,TransmittableThreadLocal解决了使用线程池创建线程时父子线程ThreadLocal传递的问题。

        3、redisson提供了异步的分布式锁加锁解锁机制,即rLock.tryLockAsync(threadId)、rLock.unLockAsync(threadId);

上代码:

    @Override
    public void testLock() throws InterruptedException {

        final RLock rLock = redissonClient.getLock("test");
        // 创建一个线程池可父子传递的ThreadLocal,放父线程id
        final TransmittableThreadLocal<Long> transmittableThreadLocal = new TransmittableThreadLocal<>();
        transmittableThreadLocal.set(Thread.currentThread().getId());
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        // 父线程获取分布式锁的状态
        boolean parentLock = false;
        try {
            parentLock = rLock.tryLock();
            System.out.println("主线程加锁状态:" + parentLock);

            // 线程池开启子线程
            Runnable runnable = () -> {
                RFuture<Boolean> booleanRFuture = null;
                try {
                    // 用父线程id的子线程尝试重入
                    booleanRFuture = rLock.tryLockAsync(transmittableThreadLocal.get());
                    System.out.println("子线程加锁状态:" + booleanRFuture.get());

                } catch (Exception e) {
                    log.error(e.getMessage());
                } finally {
                    try {
                        // 解锁。牢记每成功加锁一次,处理完业务后就要解锁!
                        if (null != booleanRFuture && booleanRFuture.get()) {
                            rLock.unlockAsync(transmittableThreadLocal.get());
                        }
                    } catch (InterruptedException | ExecutionException e) {
                        e.printStackTrace();
                    }
                }
            };
            executorService.execute(TtlRunnable.get(runnable));
            // 休眠模拟主线程业务执行
            Thread.sleep(2000);
        } catch (Exception e) {
            log.error("父线程异常!{}", e.getMessage());
        } finally {
            // 移除threadLocal对象,谨防内存泄漏!!!
            transmittableThreadLocal.remove();
            if (null != rLock && parentLock) {
                rLock.unlock();
            }
        }
    }

结论:

        通过父子线程信息传递和redisson提供的异步加解锁机制可以实现“子线程重入父线程获取的redisson分布式锁”。

        请注意,这个示例是为了说明概念而简化的。在实际应用中,你需要根据你的应用逻辑来设计线程间的通信和锁的传递。此外,还需要考虑异常处理和锁的自动释放等问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值