Redis第14讲——Redis实现分布式锁(Redission源码解析)

在多线程环境下,为了保证数据的线程安全,我们通常用加锁的方式,使同一时刻只有一个线程可以对这个共享资源进行操作,在单服务系统我们常用JVM锁——Synchronized、ReentrantLock等。然而在多台服务系统的情况下,JVM锁就无法在多个服务器之间生效了,这时候我们就需要用分布式锁来解决线程安全的问题。

分布式锁的实现方式有很多,主流的就是基于数据库、zookeeper以及redis,当然使用redis的居多,由于篇幅原因,本节就详细介绍一下使用redis实现分布式锁的几种方式。

一、SETNX实现

ps:本文重点使Redisson实现分布式锁,咱就不从SETNX+EXPIRE、SETNX+LUA脚本...什么的逐步演进了,本身就是一回事,直接一步到位,用set ex px nx+唯一校验+LUA脚本删除等操作实现。

利用Redis的单线程特性,在多个Redis客户端通过SETNX,如果返回1表示获取锁成功,反之失败。因为Redis的单线程机制,所以可以保证一个客户端成功获取后,其它客户端都会获取失败。伪代码如下:

public class RedisLock {
    private Jedis jedis;
    private void init(){
        //建立连接
        jedis = JedisPoolFactory.getJedis();
    }
    /**
     * 获取锁
     * @param lockKey 锁的键值
     * @param requestId 唯一标识
     * @param expireTime 过期时间
     * @return 是否获取锁 成功返回true,反之false
     */
    public boolean tryLock(String lockKey,String requestId,int expireTime){
        //2、加锁
        String result = jedis.set(lockKey,requestId,"NX","EX",expireTime);
​
        return "OK".equals(result);
    }
​
    /**
     * 释放锁
     * @param lockKey 锁的键值
     * @param requestId 唯一标识
     * @return 成功true,失败false
     */
    public boolean unlock(String lockKey,String requestId){
        //LUA脚本:判断当前锁的值是否等于请求标识requestId,如果是则删除锁并返回true,反之返回false。
        String scripts = "if redis.call('get',KEYS[1]) == ARGV[1] then " +
                "return redis.call('del',KEYS[1]) else return 0 end";
        Object result = jedis.eval(scripts, Collections.singletonList(lockKey), Collections.singletonList(requestId));
        return Long.parseLong(result.toString())==1L;
    }
}

存在的问题:

  • 锁无法续期:假设线程A获取了锁,但是由于网络原因,执行时间超过了设置的过期时间,这是锁被释放了,线程B获取锁成功,此时线程A和B都会执行临界区的代码,这是绝对不允许的。

二、Redisson实现分布式锁

在使用SETNX实现的分布式锁中,存在锁无法续期导致并发冲突的问题。不过这个问题在Redisson中用看门狗的机制巧妙地解决了,这也是我们实现分布式锁最常用的方式。

2.1 整体类图

标黄的两个类就是我们今天的重点,看门狗续期的实现逻辑在RedissionBaseLock类中,加锁逻辑在RedissionLock类中。

2.2 大致流程

在深入代码前,我们先看下加锁、看门狗续期大致的流程,有个大致印象。

2.3 加锁流程源码分析

下面我们就按上面的流程图,走走源码。

2.3.1 lock()---加锁入口

  • lock方法,一个没设置过期时间,一个设置了过期时间。

解析:

  • 第一个红框:尝试加锁,会返回null或者具体的值,返回null表示加锁成功,反之有线程持有该锁,加锁失败。
  • 第二个红框:加锁失败,while循环不断尝试。

2.3.2 tryAcquire()---执行加锁LUA脚本并判断是否要进行锁续期

  • 第一个红框:执行加锁LUA脚本,返回null说明加锁成功,反之失败。

    • 如果设置了过期时间,第二个参数就传设置的时间。

    • 反之,使用默认的internallockLeaseTime时间。

  • 第二个红框:如果加锁成功(null),且设置了过期时间,将设置过期时间赋值给internallockLeaseTime,如果没设置,则执行scheduleExpirationRenewal方法(看门狗)。返回结果。

ps:internallockLeaseTime默认就是30s。

2.3.3 tryLockInnerAsync()-----选择slot槽并执行lua脚本

我们先看如何执行LUA加锁脚本的,这里面有点深。。。

 

slot槽这里就不多讲了。。。我们回到LUA脚本。

  • 首先检查锁的key是否存在,如果不存在,判断是否是同一线程再次过来对同一个key进行加锁,也就是当前key是否被当前线程持有(可重入性)。
  • 如果上述两个条件任意一个成立,则对当前key执行自增和设置过期时间操作,并返回null表示加锁成功。
  • 反之,返回当前锁的过期时间,表示加锁失败。

2.4 watch dog源码分析

2.4.1 scheduleExpirationRenewal()--锁续期入口

当加锁成功,且没有设置过期时间,执行scheduleExpirationRenewal()方法,这也是我们常说的"看门狗"的实现逻辑。

  • 第一个红框:EXPIRATION_RENEWAL_MAP存放续期任务,get有值说明当前锁需要续期,为null则不需要再续期了。
  • 第二个红框,执行续期操作。

2.4.2 renewExpiration()----执行锁续期操作

这个方法用netty的时间轮进行续期。

  • 第一个红框:首先会从EXPIRATION_RENEWAL_MAP中获取一个值,如果为null,就不续期了,说明这个锁可能已经被释放或过期了。

  • 第二个红框:基于TimerTask实现一个定时任务,设置internalLockLeaseTime / 3的时长进行一次锁续期,也就是每10s进行一次续期。

    • 这里也会从EXPIRATION_RENEWAL_MAP里获取一个值,检查锁是否被释放或者过期了。

    • 如果不为null,则获取第一个等待该锁的线程,如果没有等待也就说明此时没有竞争,也同样不需要续期了。

    • 如果有等待的线程,说明需要续期,它会异步调用renewExpirationAsync(threadId)方法来实现续期。

    • 当异步续期操作完成,会调用whenComplete方法来处理结果,如果有异常,则将该锁从EXPIRATION_RENEWAL_MAP中移除。如果续期成功,则会重新调用renewExpiration()方法进行下一次续期,如果续期失败,则调用cancelExpirationRenewal()方法取消续期。

2.4.3 renewExpirationAsync()--执行锁续期LUA脚本

  • 如果当前key存在,说明当前锁还被该线程持有,那么就重置过期时间为30s,并返回true,表示续期成功,反之返回false。

2.4.4 cancelExpirationRenewal---取消锁续期

  • 还是从这个map里获取键值对,如果为null,说明续期任务不存在,也没必要进行下去了,直接返回。
  • 如果threadId不为null,直接将这个续期任务从task里移除。
  • 如果threadId为null或者task中不再有任何线程在等待续期,此时就调用cancel方法来取消定时任务,然后在从EXPIRATION_RENEWAL_MAP中移除该续期任务。

ps:当unlock的时候也会调该方法,来执行取消锁续期的操作。

2.5 小结

2.5.1 什么时候会进行锁续期

加锁时,如果没有指定过期时间,则默认过期时间为30s且每隔10s进行锁续期操作。

ps:参考2.3.2和2.4.2小节。

2.5.2 什么情况会停止续期

  • 锁被释放。

  • 没有其它线程竞争当前锁资源。

  • 续期时发生异常。

  • 执行锁续期LUA脚本失败。

  • Redission的续期时Netty时间轮(TimerTask、TimeOut、Timer)的,并且操作都是基于JVM,所以当应用宕机、下线或重启后,续期任务也没有了。

ps:参考2.4.3小节。

2.6 lock()和trylock()的区别

讲了半天忘了说使用redission实现分布式锁的示例了😂,索性就在这补充一下吧。

lock():

RLock lock = redisson.getLock("MyLock");
lock.lock();//阻塞方法,知道获取到锁
try {
    //业务代码
}finally {
    //当前锁存在且被当前线程持有
     if(lock.isLocked() && lock.isHeldByCurrentThread()){
      //释放锁
      lock.unlock();
   }
}

lock的原理是以阻塞的方式获取锁,如果获取失败则一直等待,直到获取成功。

ps:可以参考2.3.1小节。

trylock():

RLock lock = redisson.getLock("MyLock");
boolean b = lock.tryLock();//非阻塞方法,立即返回获取结果
if(b){
     try {
          //业务代码
     }finally {
          //当前锁存在且被当前线程持有
          if(lock.isLocked() && lock.isHeldByCurrentThread()){
               //释放锁
               lock.unlock();
           }
     }
}else {
   //获取锁失败,处理逻辑
}

tryLock是尝试获取锁,如果能获取直接返回true,如果无法获取,它会按照我们指定的超时时间进行阻塞,这个时间内还会尝试获取锁,如果超过这个时间还没获取到,直接返回false。如果没有指定超时时间,就如我们的示例,那获取不到的话直接就返回false。

我们看下源码:

这是没有指定超时时间执行的方法,方法名也很见名知意,就是尝试一次加锁。指定了超时时间的这里就不介绍了,无非是在超时时间内执行while循环尝试获取锁。

三、Redission公平锁(FairLock)、联锁(MultiLock)、读写锁的使用

ps:这几种锁都不常用,所以就不细讲了,知道有这个事就行。

3.1 公平锁(FairLock)

RLock lock = redisson.getFairLock("MyLock");
lock.lock();

3.2 联锁(MultiLock)

RLock lock1 = redisson.getLock("MyLock1");
RLock lock2 = redisson.getLock("MyLock2");
RLock lock3 = redisson.getLock("MyLock3");
RedissonMultiLock lock=new RedissonMultiLock(lock1,lock2,lock3);
//同时加锁lock1、lock2、lock3
//所有的锁都上锁成功才算成功
lock.lock();
 //...
lock.unlock();

3.3 读写锁

RReadWriteLock lock = redisson.getReadWriteLock("myLock");
//读锁
lock.readLock().lock();
//写锁
lock.writeLock().lock();

ps:还有个RedLock,这个可以细讲一下,但是由于篇幅原因,就放在下一篇文章吧。

四、Redission实现分布式锁存在的问题

Redission使用看门狗续期的方案在大多数场景下是挺不错的,但在极端情况下还是会存在问题,比如:

  • 线程1首先获取锁成功,将键值对写入redis的master节点。
  • 在redis将master数据同步到slave节点之前,master故障了。
  • 此时会触发故障转移,将其中一个slave升级为master。
  • 但新的master并没有线程1写入的键值对,因此如果此时来个线程2,也同样可以获取到锁,这就违背了锁的初衷。

这个场景就是我们常说的集群脑裂(网络分区)问题。

那么比较主流的解决方案就是Redis作者提出的RedlockZookeeper实现的分布式锁,这个我们下节再讲。

End:希望对大家有所帮助,如果有纰漏或者更好的想法,请您一定不要吝啬你的赐教🙋。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

数字科技助力垃圾分类展厅,增强内容交互新体验!

如今,许多行业都开始运用数字技术,探索其在展览展示领域中的应用,其中垃圾分类展厅作为现代城市文明建设的重要一环,也通过这些技术的运用,打造出了更加生动且富有科技感的展示空间,它不仅提升公众对垃圾分…

原生微信小程序中案例--仿boss区域树选择列多选功能

1. 需求描述: 区域三级列表, 有添加,编辑,删除功能。 选择父级分类,其下子类全部选中,当前分类后加标志显示全字样取消选中子类,其父类分类后标志显示选中数量若子类全部选中,除当…

神经网络鸢尾花分类

⚠申明: 未经许可,禁止以任何形式转载,若要引用,请标注链接地址。 全文共计3077字,阅读大概需要3分钟 🌈更多学习内容, 欢迎👏关注👀【文末】我的个人微信公众号&#xf…

【超简单实用】Zotero 7 内置pdf背景颜色更改插推荐以及安装

Zotero beta7 pdf 内置颜色更换 zetore 6 很多成熟的插件在 zetore 7都不能用了。版本回退看起来内置文章的注释会被消除,所以又不想退回去。前几个月在找beta 7 的pdf 护眼色的插件一直没有,今天终于发现了!!!&#…

《架构风清扬-Java面试系列第26讲》聊聊的LinkedBlockingQueue的特点及使用场景

LinkedBlockingQueue也是BlockingQueue接口的一个实现类之一 这个属于基础性问题,老规矩,我们将从使用场景和代码示例来进行讲解 来,思考片刻,给出你的答案 1,使用场景 实现:基于链表实现的阻塞队列&#…

Django5框架之多重继承

在Django模型中也支持使用多重继承,这点与Python语法中的继承是一致的。Django模型多重继承就是同时继承多个父类模型,父类中第一个出现的基类(如:Meta类)是默认被使用的。如果存在多个父类包含Meta类的情况&#xff0…

Redis - Set 集合

前言 集合类型可保存多个字符串类型的元素,但和列表类型不同的是,集合中的元素之间是⽆序的(顺序不重要,变换一下集合中的数据顺序,集合不会发生改变) 的并且元素不允许重复 ⼀个集合中最多可以存储 2^32-1…

echarts 堆叠柱状图 顶部添加合计

堆叠有3个,后面加了一个对象显示顶部的数据, 其实主要的代码还是在series 的第四项,需要注意的是 series的第四项中的data需要为 data: [0, 0, 0] 顶部的统计才能显示出来 增加的代码如下 {name: 综合,type: bar,stack: total,label: {sh…

基于springboot+vue+Mysql的篮球竞赛预约平台

开发语言:Java框架:springbootJDK版本:JDK1.8服务器:tomcat7数据库:mysql 5.7(一定要5.7版本)数据库工具:Navicat11开发软件:eclipse/myeclipse/ideaMaven包:…

STM32 HAL库F103系列之DAC实验(二)

DAC输出正弦波实验 实验简要 1,功能描述 通过DAC1通道1(PA4)输出正弦波,然后通过DS100示波器查看波形 2,使用定时器7 TRGO事件触发转换 TEN1位置1、TSEL1[2:0]010 3,关闭输出缓冲 BOFF1位置1 4,使用DMA模式 DMAE…

无人机+遥控器:工业级手持地面站(支持安卓系统)功能技术详解

手持地面站是一种专为无人机设计的便携式设备,用于实现飞行控制、任务规划、数据链路通信等功能。由于支持安卓系统,这种地面站设备在软件生态上具有极大的灵活性,能够兼容并运行众多基于安卓平台的无人机控制应用程序。 在硬件方面&#xff…

vue中的mixin(局部混入、全局混入)

一、mixin是什么 Mixin是面向对象程序设计语言中的类,提供了方法的实现。其他类可以访问mixin类的方法而不必成为其子类;Mixin类通常作为功能模块使用,在需要该功能时“混入”,有利于代码复用又避免了多继承的复杂 Vue中的mixin…

如何在 Ubuntu 14.04 上配置 StatsD 以收集 Graphite 的任意统计数据

介绍 Graphite 是一个图形库,允许您以灵活和强大的方式可视化不同类型的数据。它通过其他统计收集应用程序发送给它的数据进行图形化。 在之前的指南中,我们讨论了如何安装和配置 Graphite 本身,以及如何安装和配置 collectd 以编译系统和服…

python实现视频剪辑

即刻关注,获取更多 实现目标 因上传某盘等文件大小限制,无法上传视频,故需要对视频进行压缩 参考资料 ffmpeg文档参考: https://ffmpeg.org/ffmpeg.html 依赖条件 已经安装好python3.11 ,原则上更高版本也可以 安装 ffmpeg 依赖 p…

21.3K star!推荐一款可视化自动化测试/爬虫/数据采集神器!功能免费且强大!

大家好,我是狂师! 在大数据时代,信息的获取与分析变得尤为重要。对于开发者、数据分析师乃至非技术人员来说,能够高效地采集网络数据并进行分析是一个强有力的工具。今天,我要向大家推荐的是一款功能强大、操作简单且…

线上申报开放时间!2024年阜阳市大数据企业培育认定申报条件、流程和材料

2024年阜阳市大数据企业培育认定申报条件、流程和材料,线上申报开放时间整理如下 一、2024年阜阳市大数据企业培育认定申报要求 (一)经营范围 申请认定的企业应当从事以下生产经营活动: 1.从事数据收集、存储、使用、加工、传输、…

HotSpot JVM 中的应用程序/动态类数据共享

0.前言 本文的目的是详细讨论 HotSpot JVM 自 JDK 1.5 以来提供的一项功能,该功能可以减少启动时间,但如果在多个 JVM 之间共享相同的类数据共享 (CDS) 存档,则还可以减少内存占用。 1.类数据共享 (CDS) CDS 的想法是使用特定格式将预处理…

多分辨率展开

1.,首先这个图居然给出了基和对偶基相等这个概念。我需要说明一下这个概念的来源。 1.1.,对偶基一开始是来自高等代数的线性空间,然后是泛函分析中的赋范线性空间的共轭空间。至于基的概念,赋范线性空间并没有,可能是因为正交需要内积来定义&…

Python基础知识(二)

🎬 秋野酱:《个人主页》 🔥 个人专栏:《Java专栏》 《Python专栏》 ⛺️心若有所向往,何惧道阻且长 文章目录 1.输入和输出函数1.1输出函数1.2输入函数 2.常见运算符2.1赋值运算符2.2比较运算符2.3逻辑运算符2.4and逻辑与2.5or逻辑或2.6not逻…

超纯水抛光树脂MB-115的使用及装填注意事项

用前须知 01.抛光树脂是由高度纯化、转型的H型阳树脂和OH型阴树脂混合而成,如果装填和操作得当,在最初的周期中即可制备出电阻率大于18.0MΩcm和TOC小于10ppb的超纯水,无需化学再生。 02.树脂开封后长时间暴露在空气中会吸收二氧化碳&#…