Redisson源码(一)RedissonLock加锁与解锁过程原理分析

在当今分布式微服务架构流行的情况下,显然在传统单体项目中使用的JDK自带的锁已经不能解决资源竞争的问题了, 进而出现的解决方案有1)利用数据库 2)redis 3)zookeeper,经过验证的是利用redis做分布式锁无论在可用性、可靠性上比较有优势。 而使用Redisson来做分布式锁很多人在熟悉不过了,它提供的Lock就是基于redis来做的。

Tip以下是本人经过多年的工作经验集成的JavaWeb脚手架,封装了各种通用的starter可开箱即用,同时列举了互联网各种高性能场景的使用示例。

ruby 复制代码
// Git代码
https://gitee.com/yeeevip/yeee-memo
https://github.com/yeeevip/yeee-memo

1 Redisson锁的基本使用

JDK提供的Lock、CountDownLatch、Semaphore等解决资源竞争/协调的并发辅助工具API在Redisson框架同样也提供了,区别就是它是支持分布式的, 并且使用的方式也和JDK中大同小异。

1.1 执行流程

1.2 Lock的使用示例

我们来看一下最基本的使用示例代码:

csharp 复制代码
public class LockExample {
    
    public void testLock() {
        RLock lock = redissonClient.getLock("testLockKey");
        // 1.最常用的使用方法
        lock.lock();
        try {
            // 2.执行业务
            doSomeThing();
        } finally {
            // 3.解锁
            lock.unlock();
        }
    }
}

Tip我工作中Redisson各种锁的具一些体使用示例Demo,有兴趣的可以参考一下

ruby 复制代码
// Git代码
https://gitee.com/yeeevip/yeee-memo/tree/master/solution-problem/distribute-lock/distribute-lock-redisson/src/main/java/vip/yeee/memo/demo/distribute/lock/redisson/example

2 加锁过程分析

2.1 代码执行分析

  • 1.通过redissonClient调用getLock()方法获取到锁对象

getLock获取到的是非公平锁,非公平锁不严格要求先到先得,后面申请加锁的线程有可能会比之前等待的提前拿到锁。

  • 2.然后通过锁对象调用lock()方法进行加锁
  • 3.使用当前线程ID执行tryAcquire()尝试获取锁,若获取到锁直接返回

这里tryAcquire()方法会调用redis服务器执行Lua脚本命令获取锁;

还有一点就是加锁成功后会有watchdog机制给锁续期防止业务执行时锁释放了。

  • 4.若没有获取到锁时,会给该线程向channel订阅锁通知,同时继续自旋尝试获取锁

向channel订阅后,锁释放后可以收到通知立即去尝试获取锁;

自旋获取并不是说直接无限调用tryAcquire,而是根据上次tryAcquire返回的ttl阻塞等待后才再次tryAcquire

2.1.1 Java核心代码

scss 复制代码
public class RedissonLock {
    
    private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) {
        // 获取当前线程ID
        long threadId = Thread.currentThread().getId();
        
        // 尝试加锁,返回值为需要等待的时间,若为空则表示当前线程加锁成功
        Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
        if (ttl == null) {
            return;
        }
        
        // 尝试加锁若没有成功
        
        // 1.该线程向channel订阅锁通知
        RFuture<RedissonLockEntry> future = subscribe(threadId);
        ...

        try {
            while (true) {
                ttl = tryAcquire(-1, leaseTime, unit, threadId);
                
                // ttl为null,说明获取成功,返回
                if (ttl == null) {
                    break;
                }
                // 若锁有效期ttl大于0,则阻塞ttl后继续获取,防止空自旋
                if (ttl >= 0) {
                    try {
                        future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                    } catch (InterruptedException e) {
                        if (interruptibly) {
                            throw e;
                        }
                        future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                    }
                } else {
                    ...
                }
            }
        } finally {
            unsubscribe(future, threadId);
        }
    }
}

2.1.2 Lua脚本代码

ini 复制代码
-- KEYS[1] = 锁对象唯一键
-- ARGV[1] = 锁过期时间
-- ARGV[2] = 加锁线程ID

-- 还没有线程加锁
if (redis.call('exists', KEYS[1]) == 0) then
-- 当前线程占有锁
redis.call('hincrby', KEYS[1], ARGV[2], 1);
-- 并设置过期时间
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;

-- 若该线程已经持有锁
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
-- 重入次数+1
redis.call('hincrby', KEYS[1], ARGV[2], 1);
-- 设置过期时间
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;

-- 锁被其他线程占有,则返回剩余有效期
return redis.call('pttl', KEYS[1]);

3 解锁过程分析

3.1 代码执行分析

解锁逻辑相对比较简单

  • 1.使用unlock()方法进行解锁
  • 2.根据当前线程再调用unlockInnerAsync()方法执行lua命令解锁

3.1.1 Java核心代码

csharp 复制代码
public class RedissonLock {
    public void unlock() {
        ...
        unlockAsync(Thread.currentThread().getId());
        ...
    }
    public RFuture<Void> unlockAsync(long threadId) {
        ...
        // 这个方法去执行Lua命令
        unlockInnerAsync(threadId);
        ...
    }
}

3.1.2 Lua脚本代码

lua 复制代码
-- KEYS[1] = 锁对象唯一键
-- KEYS[2] = 该锁发布订阅channel
-- ARGV[1] = UNLOCK_MESSAGE
-- ARGV[2] = 锁过期时间
-- ARGV[3] = 加锁线程ID

-- 该线程未持有锁直接返回
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
return nil;
end;

-- 该线程重入次数-1
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);

-- 如果重入次数还大于0
if (counter > 0) then
-- 重置该线程锁过期时间
redis.call('pexpire', KEYS[1], ARGV[2]);
return 0;
-- 重入次数等于0了
else
-- 释放锁
redis.call('del', KEYS[1]);
-- 向channel发布锁释放消息
redis.call('publish', KEYS[2], ARGV[1]);
return 1;
end;
return nil;

最后

Redission锁的原理我大致已经介绍完了,有问题的可以私聊我或者评论区留意~ 之后我会介绍Redission的延迟队列原理,感兴趣的话持续关注哦。

Tip以下是本人经过多年的工作经验集成的JavaWeb脚手架,封装了各种通用的starter可开箱即用,同时列举了互联网各种高性能场景的使用示例。

ruby 复制代码
// Git代码
https://gitee.com/yeeevip/yeee-memo
https://github.com/yeeevip/yeee-memo
相关推荐
重生之我要进大厂几秒前
LeetCode 876
java·开发语言·数据结构·算法·leetcode
_祝你今天愉快4 分钟前
技术成神之路:设计模式(十四)享元模式
java·设计模式
小筱在线42 分钟前
SpringCloud微服务实现服务熔断的实践指南
java·spring cloud·微服务
luoluoal1 小时前
java项目之基于Spring Boot智能无人仓库管理源码(springboot+vue)
java·vue.js·spring boot
ChinaRainbowSea1 小时前
十三,Spring Boot 中注入 Servlet,Filter,Listener
java·spring boot·spring·servlet·web
小游鱼KF1 小时前
Spring学习前置知识
java·学习·spring
扎克begod1 小时前
JAVA并发编程系列(9)CyclicBarrier循环屏障原理分析
java·开发语言·python
青灯文案11 小时前
SpringBoot 项目统一 API 响应结果封装示例
java·spring boot·后端
我就是程序猿1 小时前
tomcat的配置
java·tomcat
阳光阿盖尔1 小时前
EasyExcel的基本使用——Java导入Excel数据
java·开发语言·excel