【redis-04】Redisson实现分布式锁实战和源码剖析

news2024/11/18 9:50:14

redis系列整体栏目


内容链接地址
【一】redis基本数据类型和使用场景https://zhenghuisheng.blog.csdn.net/article/details/142406325
【二】redis的持久化机制和原理https://zhenghuisheng.blog.csdn.net/article/details/142441756
【三】redis缓存穿透、缓存击穿、缓存雪崩https://zhenghuisheng.blog.csdn.net/article/details/142577507
【四】redisson实现分布式锁实战和源码剖析https://zhenghuisheng.blog.csdn.net/article/details/142646301

如需转载,请输入:https://blog.csdn.net/zhenghuishengq/article/details/142577507

redisson实现分布式锁实战和源码剖析

  • 一,redisson实现分布式锁实战和源码剖析
    • 1,redis原生方式实现分布式锁
    • 2,Redisson实现分布式锁
      • 2.1,ReentrantLock 实现锁
      • 2.2,Redission实现分布式锁案例
      • 2.3,Redission底层实现原理和源码剖析
        • 2.3.1,lock加锁逻辑
        • 2.3.2,lock锁续命逻辑
        • 2.3.3,加锁失败阻塞逻辑
        • 2.3.4,unlock锁释放
    • 3,Redisson总结

一,redisson实现分布式锁实战和源码剖析

前面几篇讲解了redis的基本数据类型,接下来在本文中,讲解一下如何通过redis实现一把分布式锁。在分布式环境中,所有的jvm层面的锁将会失去该有的作用,因此在分布式环境中,可以通过redis来实现这种分布式锁,说白了就是在分布式和高并发的环境下,将并行的线程改成串行。

1,redis原生方式实现分布式锁

在redis内部,提供了实现分布式锁的方式,可以直接通过 setnx 命令的方式来,加下来直接通过代码的方式来演示一段扣减库存的代码,比如以下这段代码,对id为1001的手机进行扣减库存,此时redis中设置了库存为100

set phoneCount 100

随后自定义一把分布式锁,在设置 key/value 的同时,并设置过期时间,可以保证整条命令的原子性

@RestController
@RequestMapping("/test")
@Slf4j
public class StockController {
    @Resource
    private RedisTemplate redisTemplate;
    //手机id
    public static final String PHONE_ID = "phone:1001";
    //手机数量
    public static final String PHONE_COUNT = "phoneCount";

    @GetMapping("/disStock")
    public AjaxResult disStock(){
        //每个线程分配一个唯一标识
        String flag = UUID.randomUUID().toString();
        //定义一把分布式锁,设置有效期为30s
        redisTemplate.opsForValue().setIfAbsent(PHONE_ID, flag, 30, TimeUnit.SECONDS);
        try {
            //当前库存
            Integer stock = Integer.parseInt(redisTemplate.opsForValue().get(PHONE_COUNT) + "");
            if (stock > 0){
                stock = stock - 1;
                redisTemplate.opsForValue().set(PHONE_COUNT,stock);
                log.info("当前库存值为:" + stock);
            }else{
                log.info("当前库存为空,扣减失败");
            }
        }finally {
            redisTemplate.delete(PHONE_ID);
        }
        return AjaxResult.success();
    }
}

如果是在并发量不大,或者说能接受超卖的情况下,上面这种方式实现分布式锁是够用的。

当然上面这种方式实现也有问题,就是有可能锁被误删的问题。假设此时线程1先拿到锁,线程2来时发现线程1已经拿到锁,那么线程2就会等待线程1执行完。但是现在还有问题,锁设置了一个过期时间30s,假设说线程1在执行下面这段代码的时候,可能逻辑特别复杂执行时间超过了30s,假设需要花费40s才能完成,那么在30s的时候,锁就过期了,那么线程2就能去抢锁

if (stock > 0){
	stock = stock - 1;
    xxxxx		//业务需要执行40s
 	redisTemplate.opsForValue().set(PHONE_COUNT,stock);
	log.info("当前库存值为:" + stock);
}

但是线程1还是在执行的,假设此时线程2正拿到锁在执行任务,在40s后线程1执行完的时候,直接把这把锁给删了,导致线程2锁又失效了,线程2又没执行完,后面又会执行扣库存,删锁的命令,这样就会导致后面的线程的锁都会被莫名其妙的删除,库存方面最终也会出现超卖的问题。

在这里插入图片描述

redisTemplate.delete(PHONE_ID);

而且还会导致超卖问题,如线程1还没有set减1的操作到redis中,线程2拿到的还是100,按理来说是线程1减掉的值99,然后还是对100进行操作,如果是在高并发环境下,就会严重的出现超卖的问题。

因此需要在删锁时做一个进一步的优化,判断一下加锁的唯一标识是不是当前线程的唯一标识,是的话才能删

if(flag.equals(redisTemplate.opsForValue().get(PHONE_ID))){
	redisTemplate.delete(PHONE_ID);
}

上面这种情况在系统稳定的时候进行释放锁时没有问题,但是也可能遇到极端的情况,比如在释放锁之前遇到系统卡顿的情况,导致还没执行释放锁的命令锁又过期了,这样别的线程又能抢锁,然后当前线程在执行到删除锁命令的时候,有把别的抢到锁的线程的锁给删除了,又出现了上面的这个 超卖 的问题

if(flag.equals(redisTemplate.opsForValue().get(PHONE_ID))){
    xxxx 		//系统卡顿
	redisTemplate.delete(PHONE_ID);
}

总而言之如果用redis的自定义分布式锁时,会由于锁的超时时间、锁过期和锁删除机制,会导致出现 超卖问题。其主要原因是锁会过期,这样导致未执行完的线程还会对锁进行删除的操作,导致其他线程锁失效。

2,Redisson实现分布式锁

虽然说redis确实可以通过自定义的方式实现一把分布式锁,但是其内部确实还存在一些问题,如经典的超卖问题,其主要原因还是,不能控制每个线程的过期时间,导致如果某个线程超时的话,就会出现锁提前释放,后续也可能出现将其他线程的锁删除的行为

因此出现了redisson分布式锁的实现,其官网链接地址如下:https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95

在这里插入图片描述

2.1,ReentrantLock 实现锁

看到这些可重入锁,公平锁等等,可以联想到JDK内部的JUC的实现,如可以查看本人写的JUC系列的 ReentrantLock的实现: https://blog.csdn.net/zhenghuishengq/article/details/132857564

实现一把单JVM进程锁的方式如下,在定义完一把 ReentrantLock 锁之后呢,直接调用内部两个简单的api就能实现加锁和解锁,底层通过aqs实现

ReentrantLock lock = new ReentrantLock();
lock.lock();
lock.unlock()

底层通过clh同步等待队列实现,同时还支持公平锁和非公平锁,由于本文主角是redission,因此详细可以去看上面给的文章的链接

在这里插入图片描述

2.2,Redission实现分布式锁案例

接下来针对上面这段自定义实现的分布式锁,通过redisson进行优化

public class StockController {

    @Resource
    private RedisTemplate redisTemplate;

    @Resource
    private Redisson redisson;

    //手机id
    public static final String PHONE_ID = "phone:1001";
    //手机数量
    public static final String PHONE_COUNT = "phoneCount";

    @GetMapping("/disStock")
    public AjaxResult disStock(){
        String flag = UUID.randomUUID().toString();
        //定义一把分布式锁,设置有效期为30s
        RLock lock = redisson.getLock(PHONE_ID);
        lock.lock();
        try {
            //当前库存
            Integer stock = Integer.parseInt(redisTemplate.opsForValue().get(PHONE_COUNT) + "");
            if (stock > 0){
                stock = stock - 1;
                redisTemplate.opsForValue().set(PHONE_COUNT,stock);
                log.info("当前库存值为:" + stock);
            }else{
                log.info("当前库存为空,扣减失败");
            }
        }finally {
            lock.unlock();
        }
        return AjaxResult.success();
    }
}

通过上面这段代码可以发现,其内部的实现方式和ReentrantLock的实现是很像的,都是在获取到相对于的实例对象之后,通过lock方式加锁和通过unlock的方式进行解锁

RLock lock = redisson.getLock(PHONE_ID);
lock.lock();
lock.unlock();

2.3,Redission底层实现原理和源码剖析

2.3.1,lock加锁逻辑

在看源码之前,来先对加锁内部做一个预想,无非就是抢锁、没抢到的阻塞,阻塞的线程轮询抢锁。

接下里进入内部源码查看,先进入这个lock方法,然后进入这个 lockInterruptibly 方法,首先会获取到当前线程id,需要给后面使用

在这里插入图片描述

接下来就是进入重要的 tryAcquire 方法,这个就是主要的获取锁的逻辑代码

Long ttl = this.tryAcquire(leaseTime, unit, threadId);

最后进入这个最重要的 tryLockInnerAsync 方法,内部其实是通过一个lua脚本来实现原子性,由于redis中执行任务的线程还是单线程,因此下面这一大段都可以保证操作的原子性

在这里插入图片描述

感兴趣的可以了解一下lua脚本,上面也通过箭头表明了lua脚本的参数,正常的对象都是通过 key/value 的方式表示,在lua脚本中,前面的这个集合标识key,后面的几个参数表示value,就是前面的 getName 是key,后面的 internalLockLeaseTime, getLockName(threadId) 是value,通过ARGV表示。lua脚本官方更加推荐使用一个key对应一个value,当然也允许key有1个或者多个,value有1个或者多个

Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));

接下来分析这段代码,其本质就是一个通过hset设置值,

  • key是KEYS[1],代表的是getName(),这个name由外部提供,在实例化的时候传了一个key进来redisson.getLock(PHONE_ID),那么这个name此时表示的就是外部设置的手机id。
  • ARGV[2]表示第二个参数,对应的是 getLockName(threadId) ,就是线程id
  • 最后通过pexpire设置一个过期时间,此时的 ARGV[1]表示的是 internalLockLeaseTime,在内部定义了这个时间 private long lockWatchdogTimeout = 30 * 1000; 默认就是30s,看名字就知道是一个看门狗的超时机制
  • 如果设置成功,那么就回返回一个nil值,对应java里面的null值
"if (redis.call('exists', KEYS[1]) == 0) then " +
	"redis.call('hset', KEYS[1], ARGV[2], 1); " +
	"redis.call('pexpire', KEYS[1], ARGV[1]); " +
	"return nil; " +
"end; "

redis通过这种管道的方式实现lua脚本,减少网络开销,同时保证操作执行的原子性,在redis官方也有介绍,可以直接通过lua脚本代替redis的事务。

2.3.2,lock锁续命逻辑

上面的讲解的就是 tryLockInnerAsync 方法,通过异步的方式去拿到锁,通过Future阻塞拿到执行任务的结果,拿到执行结果之后,再回调一下这个 addListener 方法

在这里插入图片描述

接下来主要是看这个 addListener 中的核心方法 scheduleExpirationRenewal ,第一眼可以看到里面就是一个定时任务的线程类,看默认就是会在 internalLockLeaseTime / 3 时间内执行一次,也就是10s后执行一次

在这里插入图片描述

这一块的内部实现延时通过lua脚本,实现锁续命机制。其续命逻辑也很简单,如果10s后线程还没执行完成,内部会通过递归的方式循环调用,继续调用这个 scheduleExpirationRenewal 方法,很多中间件实现这种续命的方式都是采用内部递归调用的方式

//判断上一次续命是否续命成功
if (future.getNow()) {	
	// reschedule itself
    scheduleExpirationRenewal(threadId);
}
2.3.3,加锁失败阻塞逻辑

当某个线程加锁失败时,那么该线程就会设置成阻塞状态,从而让出cpu的使用权。依旧得看这个抢锁逻辑的lua脚本,看到最后一句,如果抢锁失败的话,那么就会返回一个pttl的状态,其实就是一个拿到锁的过期时间。比如拿到锁的线程1已经执行了10s,那么来拿锁的线程2就会获取到剩余20s的过期时间

在这里插入图片描述

再回到进入这个最初的抢锁方法中,可以发现每一个线程都会返回一个ttl的过期时间,首先会对这个ttl超时时间进行判断,上面抢锁的逻辑可以看到,如果拿到锁的线程会返回一个nil,就是对应java中的null,如果不为null,就会继续往下执行

在这里插入图片描述

在进行阻塞的时候,作者使用了发布订阅的模式进行了优化 ,在线程进行阻塞时,把这些队列都订阅一个主题,当有锁释放的时候,那么就唤醒订阅了这个主题的线程。

RFuture<RedissonLockEntry> future = subscribe(threadId);
commandExecutor.syncSubscription(future);

随后进入一个自旋获取锁的阶段,在JDK的 ReentrantLock 中,就是采用的自旋的方式获取锁。但是在redis分布式情况下,一般都是适用于高流量高并发的场景,因此是不能完全一直空转自旋的,而且想想默认设置30s一个线程,刚运行就让大量的线程在那空转,肯定是不合适的

接下来看内部的这段自旋的代码,接下来主要分析这段ttl大于0的情况,getLatch表示一个JDK中的Semaphore信号量,这里用来做阻塞操作,比如说获取到的ttl为15s,那么这个线程就阻塞15s在这里,再进行一次自旋抢锁,而不是像 ReentrantLock 一样一直空转自旋在那抢锁,从而降低cpu的使用率,同时通过阻塞让出cpu的使用权

try {
    while (true) {
        ttl = tryAcquire(leaseTime, unit, threadId);
        // lock acquired
        if (ttl == null) {	
            break;
        }
        if (ttl >= 0) {		//如果ttl大于0
            //
            getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
        } else {
            getEntry(threadId).getLatch().acquire();
        }
    }
} finally {
    unsubscribe(future, threadId);	//唤醒订阅的线程
}

通过这种信号量阻塞的方式,达到间歇性加锁的目的。并且在抢锁时,内部没有公平锁的概念,默认就是非公平锁。

2.3.4,unlock锁释放

在上面的加锁失败阻塞中,讲了有这段加锁失败阻塞的方法,内部提供了一个同步订阅的方法,就是每个加锁失败的线程会订阅一个topic主题,当锁被释放或者过期之后就能通知订阅的线程来抢锁,不然每次自旋抢锁就太低效了,比如5s中执行完了,设置的30s,剩余的ttl还有25s,还要等25s去抢锁,显然不合适

RFuture<RedissonLockEntry> future = subscribe(threadId);
commandExecutor.syncSubscription(future);

接下来查看这段释放锁的逻辑,通过 unlockInnerAsync 进行释放锁

在这里插入图片描述

详细查看这个 unlockInnerAsync 方法之后,内部又是一个lua脚本,主要判断锁是否存在,如果存在则进行解锁的操作,然后通过 publish 发送一条消息给订阅了这个主题的所有线程可以来抢锁,内部还包含一些可重入锁等

在这里插入图片描述

3,Redisson总结

redission主要通过lua脚本来实现加锁和解锁的操作,从而保证相关操作的原子性,其主要操作有以下步骤。

  • 线程进来先执行抢锁的操作,抢锁成功则继续往下执行业务,并且通过内部递归的方式给当前线程一个watch dog 看门狗的一个续命方式。
  • 抢锁失败线程则阻塞,并将线程关注一个发布订阅的模型,供锁释放时被唤醒。并且内部通过间接性的方式轮旋抢锁,时间间隔为当前线程结束时间的ttl
  • unlock释放锁时会往一个发布订阅的模型里面发送消息,关注了这个模型的线程接收到消息之后,则会被唤醒,从而的去进行加锁的操作

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2178966.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ubuntu切换源方式记录(清华源、中科大源、阿里源)

文章目录 前言一、中科大源二、清华源三、阿里源 前言 记录ubunut切换各个源的方式。 备注&#xff1a;更换源之后使用sudo apt-get update更新索引。 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 一、中科大源 地址&#xff1a;https://mirrors.u…

【Golang】Go语言字符串处理库--strings

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

RK3588主板PCB设计学习(一)

DCDC电路可以直接参考数据手册&#xff1a; 电源输出3A&#xff0c;回流GND也应该是3A&#xff0c;回流路径和输出路径的电流是一致的&#xff0c;不要输出路径布线很粗&#xff0c;GND回流路径很细&#xff0c;并且应该保证回流面积最小&#xff1a; 这一点讲的很到位&#xf…

《深度学习》OpenCV 指纹验证、识别

目录 一、指纹验证 1、什么是指纹验证 2、步骤 1&#xff09;图像采集 2&#xff09;图像预处理 3&#xff09;特征提取 4&#xff09;特征匹配 5&#xff09;相似度比较 6&#xff09;结果输出 二、案例实现 1、完整代码 2、实现结果 调试模式&#xff1a; 三、…

华为云LTS日志上报至观测云最佳实践

华为云LTS简介 华为云云日志服务&#xff08;Log Tank Service&#xff0c;简称 LTS&#xff09;&#xff0c;用于收集来自主机和云服务的日志数据&#xff0c;通过海量日志数据的分析与处理&#xff0c;可以将云服务和应用程序的可用性和性能最大化&#xff0c;为您提供实时、…

音乐项目总结(终)

总的来说写这个项目还是状态差了&#xff0c;前期中期写太慢&#xff0c;后期疯狂赶。 讲点对写这个项目能想起来解决的问题和写的的感触。 前期&#xff1a;当时觉得时间很充足&#xff0c;有布置算法题&#xff0c;我竟然还花三四天去学算法&#xff0c;&#xff0c;动态规划…

【软设】项目管理

【软设】项目管理 (要会根据Gantt和Pert图求关键路径&#xff0c;可以看3.3的示例来了解Pert图) 一.进度管理 进度管理 是项目管理的重要组成部分&#xff0c;旨在确保项目在规定的时间范围内完成。进度管理不仅包括项目活动的规划&#xff0c;还包括监控和控制项目活动的进…

LeetCode 热题 100 回顾8

干货分享&#xff0c;感谢您的阅读&#xff01;原文见&#xff1a;LeetCode 热题 100 回顾_力code热题100-CSDN博客 一、哈希部分 1.两数之和 &#xff08;简单&#xff09; 题目描述 给定一个整数数组 nums 和一个整数目标值 target&#xff0c;请你在该数组中找出 和为目标…

Mixture-of-Experts (MoE): 条件计算的诞生与崛起【下篇】

将 Mixture-of-Experts 应用于 Transformers 既然我们已经研究了条件计算的早期工作&#xff0c;那么我们就可以看看 MoE 在变换器架构中的一些应用。 如今&#xff0c;基于 MoE 的 LLM 架构&#xff08;如 Mixtral [13] 或 Grok&#xff09;已广受欢迎&#xff0c;但 MoE 在语…

【C++题目】7.双指针_和为 s 的两个数字

文章目录 题目链接&#xff1a;题目描述&#xff1a;解法C 算法代码&#xff1a;图解 题目链接&#xff1a; LCR 179.查找总价格为目标值的两个商品 题目描述&#xff1a; 解法 解法一&#xff08;暴力解法&#xff0c;会超时&#xff09; 两层 for 循环列出所有两个数字的组合…

网络通信(学习笔记)

InputStreamReader 是 Java 中的一个类&#xff0c;它可以将字节输入流转换为字符输入流。它可以读取字节输入流&#xff0c;并使用指定的字符集将字节解码为字符。 InputStreamReader继承了Reader类 Scanner scanner new Scanner(System.in);//这是一个控制台输入的一个类&am…

巡检机器人室内配电室应用

智能巡检系统实施背景 电力系统发展已进入电气化、自动化、智能化建设加速推进的新阶段&#xff0c;设备规模大幅增长&#xff0c;新设备、新技术加快应用&#xff0c;装备水平取得长足发展&#xff0c;与此同时设备规模大幅增长&#xff0c;新设备、新技术加快应用&#xff0…

JAVA并发编程高级——JDK 新增的原子操作类 LongAdder

LongAdder 简单介绍 前面讲过,AtomicLong通过CAS提供了非阻塞的原子性操作,相比使用阻塞算法的同步器来说它的性能已经很好了,但是JDK开发组并不满足于此。使用AtomicLong 时,在高并发下大量线程会同时去竞争更新同一个原子变量,但是由于同时只有一个线程的CAS操作会成功,…

C++ | Leetcode C++题解之第446题等差数列划分II-子序列

题目&#xff1a; 题解&#xff1a; class Solution { public:int numberOfArithmeticSlices(vector<int> &nums) {int ans 0;int n nums.size();vector<unordered_map<long long, int>> f(n);for (int i 0; i < n; i) {for (int j 0; j < i;…

蒂森电梯变频器维修CPIK40 CPIK32

维修范围&#xff1a; 1、通力电梯变频器维修&#xff1a;V3F16L,通力V3F18维修,电梯变频器V3F25维修,KDL16,KDL32维修&#xff0c;通力电梯CPU主板维修&#xff0c;806板&#xff0c;电梯安全回路板&#xff0c;LCECCB&#xff0c;LCECEB&#xff0c;电梯显示板维修&#xff…

Python编码系列—Python状态模式:轻松管理对象状态的变化

&#x1f31f;&#x1f31f; 欢迎来到我的技术小筑&#xff0c;一个专为技术探索者打造的交流空间。在这里&#xff0c;我们不仅分享代码的智慧&#xff0c;还探讨技术的深度与广度。无论您是资深开发者还是技术新手&#xff0c;这里都有一片属于您的天空。让我们在知识的海洋中…

Grafana链接iframe嵌入Web前端一直跳登录页面的问题记录

概述 公司有个项目使用到Grafana作为监控界面,因为项目方的环境极其复杂,仅物理隔离的环境就有三四个,而且每个都得部署项目,今天在某个环境测试,查看界面遇到一个比较奇怪的Grafana问题,后面针对该问题进行跟踪分析并解决,故而博文记录,用于备忘。 问题 登录项目We…

CleanMyMac X v4.12.1 中文破解版 Mac优化清理工具

在数字时代&#xff0c;我们的Mac设备承载着越来越多的重要信息和日常任务。然而&#xff0c;随着时间的推移&#xff0c;这些设备可能会变得缓慢、混乱&#xff0c;甚至充满不必要的文件。这就是CleanMyMac X发挥作用的地方。 CleanMyMac X是一款功能强大的Mac优化工具&#…

Gson将对象转换为JSON(学习笔记)

JSON有两种表示结构&#xff0c;对象和数组。对象结构以"{"大括号开始&#xff0c;以"}"大括号结束。中间部分由0或多个以”&#xff0c;"分隔的”key(关键字)/value(值)"对构成&#xff0c;关键字和值之间以":"分隔&#xff0c;语法结…

C语言 | Leetcode C语言题解之第446题等差数列划分II-子序列

题目&#xff1a; 题解&#xff1a; #define HASH_FIND_LONG(head, findint, out) HASH_FIND(hh, head, findint, sizeof(long), out) #define HASH_ADD_LONG(head, intfield, add) HASH_ADD(hh, head, intfield, sizeof(long), add)struct HashTable {long key;int val;UT_ha…