Redisson可重入锁基本使用及原理分析

基本使用

1、引入依赖

		<!--    redisson依赖    -->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.13.6</version>
        </dependency>

2、配置redisson客户端

	// 将RedissonClient对象注入IOC容器
    @Bean
    public RedissonClient redissonClient(){
        Config config = new Config();
        // 配置单节点redis地址
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        return Redisson.create(config);
    }
@Resource
RedissonClient redissonClient;

// ----
// 获取可重入锁
RLock lock = redisson.getLock("anyLock");
// 最常见的使用方法
lock.lock();

// (无参)尝试获取锁
boolean isLock = lock.tryLock();

// 重载方法1 tryLock(Long maxWaitTime,Long timeoutRelease,TimeUnit.SECONDS)
// 参数1:锁的最大等待时间(期间会重试)
// 参数2: 锁超时释放时间
// 参数3:时间单位
boolean isLock = lock.tryLock(1,10,TimeUnit.SECONDS);

// 重载方法2 tryLock(Long maxWaitTime,TimeUnit.SECONDS)
// 参数1:锁的最大等待时间(期间会重试)
// 参数2:时间单位
boolean isLock = lock.tryLock(1,TimeUnit.SECONDS);

// 释放锁
lock.unlock();

tryLock

trylock具有返回值,true或者false,表示是否成功获取锁。tryLock前期获取锁逻辑基本与lock一致,主要是后续获取锁失败的处理逻辑与lock不一致。
trylock方法

可重试

使用发布订阅模式和信号量使得锁可重试

public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
        long time = unit.toMillis(waitTime);
        long current = System.currentTimeMillis();
        long threadId = Thread.currentThread().getId();
        // 获取剩余过期时间
        Long ttl = this.tryAcquire(waitTime, leaseTime, unit, threadId);
        // 为null,则获取锁成功
        if (ttl == null) {
            return true;
        } else {
            time -= System.currentTimeMillis() - current;
            // 获取锁失败后,中途tryLock会一直判断中间操作耗时是否已经消耗锁的过期时间,如果消耗完则返回false
            if (time <= 0L) {
                this.acquireFailed(waitTime, unit, threadId);
                return false;
            } else {
                current = System.currentTimeMillis();
               // 此处为订阅锁释放事件,
               // 如果当前线程通过 Redis 的 channel 订阅锁的释放事件获取得知已经被释放
               // 则会发消息通知待等待的线程进行竞争.
                RFuture<RedissonLockEntry> subscribeFuture = this.subscribe(threadId);
                // 等待释放锁,若超过剩余等待时间,则取消订阅,
                // 并返回false,获取锁失败
                if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {
                    if (!subscribeFuture.cancel(false)) {
                        subscribeFuture.onComplete((res, e) -> {
                            if (e == null) {
                                this.unsubscribe(subscribeFuture, threadId);
                            }

                        });
                    }

                    this.acquireFailed(waitTime, unit, threadId);
                    return false;
                } else {
                    try {
                     // 减去订阅等待的时间,如果消耗完则返回false
                        time -= System.currentTimeMillis() - current;
                        if (time <= 0L) {
                            this.acquireFailed(waitTime, unit, threadId);
                            boolean var20 = false;
                            return var20;
                        } else {
                            boolean var16;
                            do {
                                long currentTime = System.currentTimeMillis();
                                ttl = this.tryAcquire(waitTime, leaseTime, unit, threadId);
                                if (ttl == null) {
                                    var16 = true;
                                    return var16;
                                }

                                time -= System.currentTimeMillis() - currentTime;
                                if (time <= 0L) {
                                    this.acquireFailed(waitTime, unit, threadId);
                                    var16 = false;
                                    return var16;
                                }
								
                                currentTime = System.currentTimeMillis();
                                // 尝试使用信号量的方式获取锁
                                // 如果ttl < time ;则使用ttl的时间尝试获取锁
                                if (ttl >= 0L && ttl < time) {
                                    ((RedissonLockEntry)subscribeFuture.getNow()).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                                // 如果ttl > time ;则使用time 的时间尝试获取锁
                                } else {
                                    ((RedissonLockEntry)subscribeFuture.getNow()).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
                                }

                                time -= System.currentTimeMillis() - currentTime;
                            // time时间大于0,继续尝试获取锁    
                            } while(time > 0L);

                            this.acquireFailed(waitTime, unit, threadId);
                            var16 = false;
                            return var16;
                        }
                    } finally {
                        this.unsubscribe(subscribeFuture, threadId);
                    }
                }
            }
        }
    }

tryLock的lua脚本

    <T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        this.internalLockLeaseTime = unit.toMillis(leaseTime);
        return this.evalWriteAsync(this.getName(), LongCodec.INSTANCE, command,
                // KEYS[1] :锁的key
                // ARGV[1] :设置锁的过期时间
                // ARGV[2] :threadId
                // 判断锁是否存在,若不存在
                "if (redis.call('exists', KEYS[1]) == 0) then\n" +
                // 创建hash表并使field属性次数加一
                "redis.call('hincrby', KEYS[1], ARGV[2], 1);\n" +
                // 设置过期时间
                "redis.call('pexpire', KEYS[1], ARGV[1]);\n" +
                 // 返回结果
                "return nil;\n" +
                "end;\n" +
                 // 锁存在,判断hash的指定字段是否存在,若存在
                "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then" +
                 //  hash的field属性次数加一
                "redis.call('hincrby', KEYS[1], ARGV[2], 1);\n" +
                 // 设置过期时间
                "redis.call('pexpire', KEYS[1], ARGV[1]);\n" +
                 // 返回结果
                "return nil;\n" +
                "end;\n" +
                // 若代码走到这里,说明锁不是当前线程的,获取锁失败并返回锁的剩余过期时间(ms)
                "return redis.call('pttl', KEYS[1]);", Collections.singletonList(this.getName()), this.internalLockLeaseTime, this.getLockName(threadId));
    }
锁可重入

使用hash结构使得锁可重入
在这里插入图片描述

上述lua脚本的流程图
在这里插入图片描述

超时续约

看门狗机制开始条件 leaseTime == -1(也就是不设置过期时间,若设置了过期时间那么就使用我们设置的过期时间,看门狗机制就不会开启)

    private RFuture<Boolean> tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
        if (leaseTime != -1L) {
            return this.tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
        } else {
            RFuture<Boolean> ttlRemainingFuture = this.tryLockInnerAsync(waitTime, this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
            ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
                if (e == null) {
                	// 获取锁成功,锁的过期时间续约
                    if (ttlRemaining) {
                        this.scheduleExpirationRenewal(threadId);
                    }

                }
            });
            return ttlRemainingFuture;
        }
    }

scheduleExpirationRenewal方法


 private void scheduleExpirationRenewal(long threadId) {
        ExpirationEntry entry = new ExpirationEntry();
        // 如果entry不存在,才添加,返回null
        // 如果entry不存在,不添加进去,返回oldEntry 
        // 这样使得锁重入
        ExpirationEntry oldEntry = (ExpirationEntry)EXPIRATION_RENEWAL_MAP.putIfAbsent(this.getEntryName(), entry);
        if (oldEntry != null) {
            oldEntry.addThreadId(threadId);
        } else {
            entry.addThreadId(threadId);
            // 第一次来,添加更新有效期定时任务
            this.renewExpiration();
        }

    }

renewExpiration方法

private void renewExpiration() {
        ExpirationEntry ee = (ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName());
        if (ee != null) {
        	// 定时任务
            Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
                public void run(Timeout timeout) throws Exception {
                    ExpirationEntry ent = (ExpirationEntry)RedissonLock.EXPIRATION_RENEWAL_MAP.get(RedissonLock.this.getEntryName());
                    if (ent != null) {
                        Long threadId = ent.getFirstThreadId();
                        if (threadId != null) {
                            RFuture<Boolean> future = RedissonLock.this.renewExpirationAsync(threadId);
                            future.onComplete((res, e) -> {
                                if (e != null) {
                                    RedissonLock.log.error("Can't update lock " + RedissonLock.this.getName() + " expiration", e);
                                } else {
                                
                                    if (res) {
											// 递归                                        
											RedissonLock.this.renewExpiration();
                                    }

                                }
                            });
                        }
                    }
                }
                //internalLockLeaseTime = 30 * 1000
                // 10s后执行该任务   
            }, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);
            ee.setTimeout(task);
        }
    }

在这里插入图片描述
renewExpirationAsync方法

    protected RFuture<Boolean> renewExpirationAsync(long threadId) {
        return this.evalWriteAsync(this.getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
        		// 判断这个锁是否是当前线程的
                "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                // 重置锁的有效期
                "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                "return 1; " +
                "end;" +
                "return 0;", Collections.singletonList(this.getName()), this.internalLockLeaseTime, this.getLockName(threadId));
    }

unlock

public RFuture<Void> unlockAsync(long threadId) {
        RPromise<Void> result = new RedissonPromise();
        RFuture<Boolean> future = this.unlockInnerAsync(threadId);
        future.onComplete((opStatus, e) -> {
        	// 取消定时任务
            this.cancelExpirationRenewal(threadId);
            if (e != null) {
                result.tryFailure(e);
            } else if (opStatus == null) {
                IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: " + this.id + " thread-id: " + threadId);
                result.tryFailure(cause);
            } else {
                result.trySuccess((Object)null);
            }
        });
        return result;
    }

cancelExpirationRenewal方法

void cancelExpirationRenewal(Long threadId) {
		// 从map取出Entry
        ExpirationEntry task = (ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName());
        if (task != null) {
            if (threadId != null) {
	            // 删除threadId
                task.removeThreadId(threadId);
            }

            if (threadId == null || task.hasNoThreads()) {
                Timeout timeout = task.getTimeout();
                if (timeout != null) {
                	// 取消定时任务
                    timeout.cancel();
                }

    			//从map中删除entry
                EXPIRATION_RENEWAL_MAP.remove(this.getEntryName());
            }

        }
    }

unlock的lua脚本

protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        return this.evalWriteAsync(this.getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                /** KEYS[1] :锁的key
                 *  KEYS[2] :锁的key
                 *  ARGV[1]
                 *  ARGV[2] :锁的有效期
                 *  ARGV[3] :threadId
                */
                // 判断锁是否是当前线程的,若等于0,说明锁不存在,直接结束
                "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
                "return nil;" +
                "end;" +
                // 锁存在,hash的field属性次数减一
                "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);" +
                // 判断field值是否大于0,
                "if (counter > 0) then" +
                // >0 重置有效期
                "redis.call('pexpire', KEYS[1], ARGV[2]);" +
                "return 0;" +
                // <=0 删除锁
                "else" +
                "redis.call('del', KEYS[1]);" +
                // 消息通知等待的线程进行竞争
                "redis.call('publish', KEYS[2], ARGV[1]);" +
                "return 1;" +
                "end;" +
                "return nil;"
                , Arrays.asList(this.getName(), this.getChannelName()), LockPubSub.UNLOCK_MESSAGE, this.internalLockLeaseTime, this.getLockName(threadId));
    }

trylock和unlock流程图

在这里插入图片描述

总结

redisson分布式锁原理:

  • 可重入:使用hash结构记录线程id和重入次数
    获取锁
    1、判断锁是否存在,不存在,获取锁成功
    2、若锁存在,判断field是不是当前线程的,若是,value++;若不是,获取锁失败;
    释放锁
    1、判断field是不是当前线程的,若是,value - -,直到value为0,删除锁;

  • 可重试:利用信号量和redis的pubsub功能实现等待、唤醒、获取锁失败的重试机制(不是无限制的等待,有等待时间)

  • 超时续约: watchDog机制(开启条件为不设置过期时间),每隔一段时间(this.internalLockLeaseTime / 3L),重置过期时间
    ps:internalLockLeaseTime = 30 * 1000 ms

本文的图片来源:https://www.bilibili.com/video/BV1cr4y1671t?p=67&spm_id_from=pageDriver&vd_source=3635fc6ba1824ef476d69a1c58b3e2ec

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值