redisson实现可重入锁原理

可重入锁

我们很多情况下会使用redis来实现分布式锁,但利用redis实现基础的分布式锁在稍微复杂情况下会存在一些问题。

  1. 不可重入

    指同一线程无法多次获取同一把锁。比如在获取完锁之后,执行完另一个方法又要再一次获取这把锁,是不允许拿到的。

  2. 不可重试

    一个请求如果第一次获取锁失败,就立即返回,不再进行重试获取。
    详细可参考:如何实现可重试锁

  3. 锁超时释放

    业务还没执行完,锁超时释放了,造成并发问题。应该可以根据业务动态延长超时时间。
    详细可参考:如何解决锁超时问题

这三点其实有很多工具可以帮忙实现,其原理是一致的,在这以redisson为例深入研究。

redisson是什么?

redisson就是一个操作redis的一个工具,更多是用来上分布式锁,他帮你写了很多固定的上锁步骤,你直接调用即可。

本篇文章主要研究redisson解决不可重入问题

1 那要实现可重入锁,其原理是什么?

在保存锁时会记录获取锁的次数。如果同一个线程递进3次获取同一把锁,那次数就是3,释放锁时,不能直接删除,而是释放一次锁,标志位就减1,直到减为0才删除锁。所以每次释放锁时,需要再判断一下标志位是否为0

需要注意的是只有判断所以存在并且是自己上的时候,才能再次将标识值+1,如果是其他线程获取的则直接获取锁失败。

redisson是基于redis实现了,那在redis里这些数据怎么保存呢?

上锁时向redis里存入hash结构的数据,hash的key为线程值,value为上锁的次数。

在这里插入图片描述

2 流程图

redisson实现可重入的原理上面已经大概讲述了,主要是多了个标识位判断次数,下面看一下整体的流程图:

在这里插入图片描述

为保证上锁的原子性同样采用lua脚本实现

在这里插入图片描述

3 redisson快速使用

下面介绍怎么快速在项目中使用redisson

  1. 添加依赖

    <dependency>
        <groupId>org.redisson</groupId>
        <artifactId>redisson</artifactId>
        <version>3.23.2</version>
    </dependency>
    
  2. 注入redisson客户端和它自带的锁操作类

    //redisson提供的客户端
    @Resource
    private RedissonClient redissonClient;
    //redisson提供的上锁工具
    @Resource
    private RLock rLock;
    
    public AjaxResult redissonTest(){
        boolean isLock = rLock.tryLock();
        if (!isLock){
            return AjaxResult.error("获取锁失败,有其他线程占用锁");
        }
        try {
            //执行业务逻辑
        } finally {
            rLock.unlock();
        }
        return AjaxResult.success();
    }
    

4 可重入实现源码

整体的流程和代码示例上面已经介绍,下面我们进入源码看下redisson是怎么实现可重入的。

我们进入rLock.tryLock()方法,选择最常用的实现类RedissonLock

在这里插入图片描述

进入this.tryLockAsync()方法,会自己获取当前线程的id,仍然选择RedissonLock实现类

public boolean tryLock() {
    return (Boolean)this.get(this.tryLockAsync());
}
public RFuture<Boolean> tryLockAsync() {
    return this.tryLockAsync(Thread.currentThread().getId());
}

进入this.tryLockAsync(Thread.currentThread().getId())方法,此处会异步执行上锁的方法

public RFuture<Boolean> tryLockAsync(long threadId) {
    return this.getServiceManager().execute(() -> {
        return this.tryAcquireOnceAsync(-1L, -1L, (TimeUnit)null, threadId);
    });
}

进入this.tryAcquireOnceAsync(-1L, -1L, (TimeUnit)null, threadId)方法,会判断一下是否传入过期时间,没有则给默认的过期时间

private RFuture<Boolean> tryAcquireOnceAsync(lon
    RFuture acquiredFuture;
    if (leaseTime > 0L) {
        acquiredFuture = this.tryLockInnerAsync(
    } else {
        acquiredFuture = this.tryLockInnerAsync(
    }
    CompletionStage<Boolean> acquiredFuture = th
    CompletionStage<Boolean> f = acquiredFuture.
        if (acquired) {
            if (leaseTime > 0L) {
                this.internalLockLeaseTime = uni
            } else {
                this.scheduleExpirationRenewal(t
            }
        }
        return acquired;
    });
    return new CompletableFutureWrapper(f);
}

进入tryLockInnerAsync方法,可以看到再次方法中执行lua脚本,脚本逻辑和上面截图的逻辑一致

//waitTime是重试持续时间
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
    return this.commandExecutor.syncedEval(
        this.getRawName()
        , LongCodec.INSTANCE
        , command
        , "if ((redis.call('exists', KEYS[1]) == 0) or (redis.call('hexists', KEYS[1], ARGV[2]) == 1)) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; return redis.call('pttl', KEYS[1]);"
        , Collections.singletonList(this.getRawName())
        , new Object[]{unit.toMillis(leaseTime)
        , this.getLockName(threadId)});
}

解锁步骤和上锁方式差不多,获取锁判断 —> 如果是自己上的就counter - 1,减后如果counter标识值不大于0了,则删除锁。进入unlockInnerAsync()方法,源码如下

protected RFuture<Boolean> unlockInnerAsync(long threadId) {
    return this.evalWriteAsync(this.getRawName()
            , LongCodec.INSTANCE
            , RedisCommands.EVAL_BOOLEAN
            , "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]); return 0; else redis.call('del', KEYS[1]); redis.call(ARGV[4], KEYS[2], ARGV[1]); return 1; end; return nil;"
            , Arrays.asList(this.getRawName()
            , this.getChannelName())
            , new Object[]{LockPubSub.UNLOCK_MESSAGE
            	, this.internalLockLeaseTime
            	, this.getLockName(threadId)
            	, this.getSubscribeService().getPublishCommand()}
            );
}

由此可见,可重入锁就是增加了个计数功能,它不会限制你获取锁的次数,但每次获取都会将计数加一。释放锁时要一层层释放知道计数为0。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猪大侠0.0

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值