Redisson分布式锁分析,可重入、可续锁(看门狗)

news2024/9/22 8:46:23

前言

  在此说明,本文章不只是讲一些抽象的概念,而是可落地的,在日常工作中基本上进行修改一下便可以使用。书接上回,上篇自研分布式锁的文章使用是一个自己手写的一个分布式锁,按照JUC里面java.util.concurrent.locks.Lock接口规范编写的。其主要逻辑为:
lock()加锁关键逻辑
  1.加锁:加锁实际上就是在redis中,给key设置一个值,为了避免死锁,并给定一个过期时间;
  2.可重入:加锁的LUA脚本,通过redis里面的hash数据模型,加锁和可重入性都要保证;
  3.自旋:加锁不成,需要while进行重试并自旋,AQS;
  4.续期:在过期时间内,一定时间内业务还未完成,自动给锁续期;
  unlock()解锁关键逻辑
  将redis的key删除,但是也不能乱删,不能说客户端1的请求将客户端2的锁给删除掉了,只能自己删自己的锁;
  考虑可重入性的递减,加锁几次就需要删除几次,最后到零了,直接del删除;上面自研的redis锁对于一般中小公司,不是特别高并发场景足够用了,单机redis小业务也撑得住。

Redis分布式锁-Redlock红锁算法

  英文名称为: Distributed locks with Redis,官方说明文档:点击此处

为什么基于故障转移的实施是不够的

  上篇文章中自己手动写的分布式锁存在的问题是单点故障时会出现数据不安全。看看官方文档怎么说?
为了理解我们想要改进的地方,让我们分析一下基于多数Redis额分布式锁库的现状。使用Redis锁定资源的最简单方法是在实例中创建一个键。使用Redis的过期功能,秘钥通常是在有限的生存时间内创建的,因此最终它会被释放。当客户需要释放资源时,它会删除秘钥。
有个问题:如果Master宕机了怎么办?主从架构条件下,添加一个副本,这种方式是不可行的。这样做我们无法实现显示互斥的安全属性,因为Redis复制是异步的。
官方文档中的一段说明进行解释,此模型存在竞争条件:
  1.Client A获取master中的锁;
  2.在对秘钥的写入传输到副本之前,主服务器崩了;
  3.副本被提升为主节点;
  4.客户端B获取对同一资源A持有锁的锁;(违反安全规定)
  有时在特殊情况下,例如在故障期间,多个客户端可以同时持有锁是完全没有问题的。如果是这种情况,您可以使用基于复制的方案。否则,我们建议实施本文档中描述 解决方案。

  简单的描述就是,当线程1首先获取锁成功,将键值写入Redis的master节点中,在Redis将该键值对同步到slave节点之前,master发生了故障;Redis触发故障转移,其中一个slave升级为新的master,此时master并不包含线程1写入的键值对,因此线程2尝试获取锁也是可以成功拿到锁的,此时相当于有两个线程获取了锁,可能会导致各种预期之外的情况发生,例如最常见的脏数据。
在这里插入图片描述
  我们加的是排它独占锁,同一时间只能有一个建Redis锁成功并持有锁,严禁出现2个以上的请求线程拿到锁。

RedLock算法设计理念

  Redis之父提出了RedLock算法解决上面这个一锁被多建的问题
  Redis也提供了Redlock算法,用来实现基于多个实例的分布式锁。 锁变量由多个实例维护,即使有实例发生了故障,锁变量仍然是存在的,客户端还是可以完成锁操作。Redlock算法是实现高可靠分布式锁的一种有效解决方案,可以在实际开发中使用。

设计理念

  该方案也是基于(set加锁、Lua脚本解锁)进行改良的,所以redis之父antirez只描述了差异的地方,大致方案如下:假设我们有N个Redis主节点,例如N= 5这些节点是完全独立的,我们不使用复制或任何其他隐式协调系统,为了取到锁客户端执行以下操作:
  1.获取当前时间,以毫秒为单位;
  2.依次尝试从5个实例,使用相同的 key和随机值(例如 UUID)获取锁。当向Redis 请求获取锁时,客户端应该设置一个超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒,则超时时间应该在5-50毫秒之间。这样可以防止客户端在试图与一个宕机的 Redis 节点对话时长时间处于阻塞状态。如果一个实例不可用,客户端应该尽快尝试去另外一个Redis实例请求获取锁;
  3.客户端通过当前时间减去步骤1记录的时间来计算获取锁使用的时间。当且仅当从大多数(N/2+1,这里是3个节点)的Redis节点都取到锁,并且获取锁使用的时间小于锁失效时间时,锁才算获取成功;
  4.如果取到了锁,其真正有效时间等于初始有效时间减去获取锁所使用的时间(步骤3计算的结果)。
  5.如果由于某些原因未能获得锁(无法在至少N/2+1个Redis实例获取锁、或获取锁的时间超过了有效时间),客户端应该在所有的Redis 实例上进行解锁(即便某些Redis实例根本就没有加锁成功,防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)。该方案为了解决数据不一致的问题,直接舍弃了异步复制只使用master节点,同时由于舍弃了slave,为了保证可用性,引入了N个节点。客户端只有在满足下面的这两个条件时,才能认为是加锁成功。
  条件1:客户端从超过半数(大于等于N/2+1)的Redis实例上成功获取到了锁;
  条件2:客户端获取锁的总耗时没有超过锁的有效时间。
该方案为了解决数据不一致的问题,直接舍弃了异步复制只使用master节点,同时由于舍弃了从节点(slave),为了保证可用性,引入了N个节点,官方建议是5。

redi只支持 AP,即高可用,为了解决CP的风险,采用N个节点,N为奇数,上面的3个master个独立,不是主从复制。为什么是奇数?N=2X+1,其中N是最终部署主机数,X是容错主机数。

什么是容错

  失败了多少个机器实例后我还是可以容忍的,所谓容就是数据的一致性还是可以的,CP数据一致性还是可以满足,加入在集群环境中,redis失败1台,可以接受。2X + 1 = 2* 1 +1 = 3, 部署3台,死了1个剩下2个可以正常工作,那就部署3台。加入在集群环境中,redis失败1台,可以接受。2X + 1 = 2* 2 +1 = 5, 部署5台,死了2个剩下3个可以正常工作,那就部署5台。

为什么是奇数

  最少的机器,最多的效果。加入集群环境中,redis失败1台,可接受。2N + 2 = 2 * 1 + 4,部署4台。

使用Redisson进行编码改造

  可重入锁,基于Redis实现的Redisson分布式可重入锁RLock java实现了java.util.concurrent.lock.Lock接口。同时还提供异步、反射和RxJava2标准额接口。
  Spring集成Redission环境开发。bom文件中引入如下代码:

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.13.4</version>
</dependency>

  配置类:RedisConfig

@Bean
public Redisson redisson() {
    Config config = new Config();
    config.useSingleServer()
        .setAddress("redis://127.0.0.1:6379")
        .setDatabase(0)
        .setPassword("123456");
    return (Redisson) Redisson.create(config);
}

  修改服务方法:InventoryService(在日常工作中的业务成层类中)

@Autowired
private Redisson redisson;
public String saleByRedisson() {
    String resMessgae = "";
    RLock redissonLock = redisson.getLock("luojiaRedisLock");
    redissonLock.lock();
    try {
        // 1 抢锁成功,查询库存信息
        String result = stringRedisTemplate.opsForValue().get("inventory01");
        // 2 判断库存书否足够
        Integer inventoryNum = result == null ? 0 : Integer.parseInt(result);
        // 3 扣减库存,每次减少一个库存
        if (inventoryNum > 0) {
            stringRedisTemplate.opsForValue().set("inventory01", String.valueOf(--inventoryNum));
            resMessgae = "成功卖出一个商品,库存剩余:" + inventoryNum + "\t" + ",服务端口号:" + port;
            log.info(resMessgae);
        } else {
            resMessgae = "商品已售罄。" + "\t" + ",服务端口号:" + port;
            log.info(resMessgae);
        }
    } finally {
        redissonLock.unlock();
    }
    return resMessgae;
}

  仔细看上面代码依然存在释放锁的问题,可能存在删除其他线程的锁,所以在finally中添加如下方法。

finally {
    // 改进点,只能删除属于自己的key,不能删除别人的
    if(redissonLock.isLocked() && redissonLock.isHeldByCurrentThread()) {
        redissonLock.unlock();
    }
}

watch dog(看门狗)自动延期机制

  源码中初始化了一个定时器,dely的时间是 internalLockLeaseTime / 3。在Redisson中,internalLockLeaseTime 是获取配置的看门狗的时间,默认是30s,也就是每隔10s续期一次,每次重新设置过期时间为30s,即从源码上看,三分之一的过期时间后会执行一次续期。总而言之,看门狗的本质在于开启一个监听线程,定期检查锁是否持有,有则延长过期时间。
Redisson看门狗续期源码:

private void renewExpiration() {
    ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (ee == null) {
        return;
    }
    Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
        @Override
        public void run(Timeout timeout) throws Exception {
            ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
            if (ent == null) {
                return;
            }
            Long threadId = ent.getFirstThreadId();
            if (threadId == null) {
                return;
            }
            RFuture<Boolean> future = renewExpirationAsync(threadId);
            future.onComplete((res, e) -> {
                if (e != null) {
                    log.error("Can't update lock " + getName() + " expiration", e);
                    return;
                }
                if (res) {
                    // reschedule itself
                    renewExpiration();
                }
            });
        }
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);

    ee.setTimeout(task);
}

  如果直接调用lock方法,客户端A加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,默认是每隔10s检查一下,如果客户端A还持有锁,就会不断的延长锁的时间。当然,如果不想使用看门狗,可以使用其他的lock带参数方法,有锁过期时间,不会有看门狗续期。
  自动续期Lua脚本源码:

protected RFuture<Boolean> renewExpirationAsync(long threadId) {
    return evalWriteAsync(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(getName()),
                          internalLockLeaseTime, getLockName(threadId));
}

Redisson多机案例

  Redis之父提出了Redlock算法解决单点故障问题,即在持有锁时,出现宕机,而master节点复制到从节点这个过程中崩了,导致没有复制到从节点上。为了解决Redis单点故障问,Redis官方号推荐使用多重锁机制解决。
  这个锁的算法实现了多redis实例的情况,相对于单redis节点来说,优点在于防止了单节点故障造成整个服务停止运行的情况且在多节点中锁的设计,及多节点同时崩溃等各种意处情况有自己独特的设计方法。
  Redisson 分布式锁支持 MultiLock 机制可以将多个锁合并为一个大锁,对一个大锁进行统一的申请加锁以及释放锁。
  最低保证分布式锁的有效性及安全性的要求如下:
  1.互斥:任何时刻只能有一个client获取锁;
  2.释放死锁:即使锁定资源的服务崩溃或者分区,仍然能释放锁;
  3.容错性:只要多数redis节点(一半以上) 在使用,client就可以获取和释放锁;
  网上讲的基于故障转移实现的redis主从无法真正实现Redlock:因为redis在进行主从复制时是异步完成的,比如在clientA获取锁后,主redis复制数据到从redis过程中崩溃了,导致没有复制到从redis中,然后从redis选举出一个升级为主redis,造成新的主redis没有clientA 设置的锁,这时clientB尝试获取锁,并且能够成功获取锁,导致互斥失效。
  使用Redisson分布式锁,需要单独的Redis master多节点,不能是哨兵模式的master或者集群模式的master;
加入现在有三台Redis 服务器,并且是master。最近推荐使用多重锁机制代码如下所示:

RLock lock1 = redisson1.getLock("lock1");
RLock lock2 = redisson2.getLock("lock2");
RLock lock3 = redisson3.getLock("lock3");

RLock multiLock = anyRedisson.getMultiLock(lock1, lock2, lock3);
// 传统而定加锁方法
multiLock.lock();
// or acquire lock and automatically unlock it after 10 seconds
lock.lock(10, TimeUnit.SECONDS);
// or wait for lock aquisition up to 100 seconds 
// and automatically unlock it after 10 seconds
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
   try {
     ...
   } finally {
       lock.unlock();
   }
}

  更具体的官方说明,详见于Redis管网

结语

  本文章到此结束,麻烦路过点赞后续还会补充内容。

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

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

相关文章

Linux根文件系统构建

直接参考【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.81 本文仅作为个人笔记使用&#xff0c;方便进一步记录自己的实践总结。 Linux“三巨头”已经完成了 2 个了&#xff0c;就剩最后一个 rootfs(根文件系统)了&#xff0c;本章我们就来学习一下根文件系统的组成以及如何构建…

苹果叶片病理分类数据集

苹果叶片病理分类数据集&#xff0c;数据集包括&#xff08;a&#xff09;健康叶片; (b) 苹果链格孢叶斑病&#xff1b; (c) 褐斑病&#xff1b; (d) 蛙叶斑病&#xff1b; (e) 灰斑&#xff1b; (f) 苹果花叶病&#xff1b; (g) 白粉病&#xff1b; (h) 叶片锈病&#xff1b; …

Redis数据结构之哈希表

这里的哈希表说的是value的类型是哈希表 一.相关命令 1.hset key field value 一次可以设置多个 返回值是设置成功的个数 注意&#xff0c;哈希表中的键值对&#xff0c;键是唯一的而值可以重复 所以有下面的结果&#xff1a; key中原来已经有了f1&#xff0c;所以再使用hse…

AI智能跟踪技术核心!

1. 目标检测技术 在视频序列的第一帧中&#xff0c;通过目标检测算法确定要追踪的目标对象的位置和大小。 技术实现&#xff1a;目标检测算法可以基于传统的图像处理技术&#xff0c;如颜色、纹理、形状等特征&#xff0c;也可以基于深度学习方法&#xff0c;如卷积神经网络&…

利用人工智能改变视频智能

人工智能视频分析正在将安全摄像头变成强大的传感器&#xff0c;可以改善您监控站点安全的方式。借助人工智能 (AI)&#xff0c;摄像头可以独立准确地检测威胁&#xff0c;而无需人工不断观看视频。 这并不奇怪——过去几年&#xff0c;这一直是安全行业协会 (SIA) 提出的几大…

Linux命令--05----find 日志

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 find1.语法语法&#xff1a;find 目标目录(路径) <选项> 参数 2.示例3.find 结合 xargs4.案例.* 模糊匹配 绝对路径 find 在 Linux 命令中&#xff0c;fin…

nginx架构篇(三)

文章目录 一、Nginx实现服务器端集群搭建1.1 Nginx与Tomcat部署1. 环境准备(Tomcat)2. 环境准备(Nginx) 1.2. Nginx实现动静分离1.2.1. 需求分析1.2.2. 动静实现步骤 1.3. Nginx实现Tomcat集群搭建1.4. Nginx高可用解决方案1.4.1. Keepalived1.4.2. VRRP介绍1.4.3. 环境搭建环境…

银河麒麟V10系统崩溃后的处理

银河麒麟V10系统崩溃后的处理 &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收藏不迷路&#x1f496; 当银河麒麟桌面操作系统V10崩溃无法启动时&#xff0c;直接使用备份还原工具不可行。此时&#xff0c;应采取以下步骤&#xff1a; 进入救援模式或LiveCD&#x…

基于Java springboot+mybatis 网上商城系统

基于Java springbootmybatis 网上商城系统 一、系统介绍二、功能展示1.主页(客户)2.登陆&#xff08;客户&#xff09;3.注册&#xff08;客户&#xff09;4.购物车(客户)5.我的订单&#xff08;客户&#xff09;6.用户管理&#xff08;管理员&#xff09;7.分类管理&#xff0…

ICM20948 DMP代码详解(35)

接前一篇文章&#xff1a;ICM20948 DMP代码详解&#xff08;34&#xff09; 上一回终于解析完了inv_icm20948_initialize_lower_driver函数&#xff0c;本回回到icm20948_sensor_setup函数&#xff0c;继续往下进行解析。为了便于理解和回顾&#xff0c;再次贴出icm20948_senso…

缓存中间件Redis进阶之路二(快速安装Redis)

一、快速安装Redis “工欲善其事&#xff0c;必先利其器”&#xff0c;在代码实战之前&#xff0c;需要在本地开发环境安装好Redis。对于不同的开发环境&#xff0c;Redis的安装及配置方式也不尽相同&#xff0c;本文将以Windows开发环境为例&#xff0c;介绍Redis的快速安装与…

Kafka 为什么这么快?

Kafka 是一款性能非常优秀的消息队列&#xff0c;每秒处理的消息体量可以达到千万级别。今天来聊一聊 Kafka 高性能背后的技术原理。 1 批量发送 Kafka 收发消息都是批量进行处理的。我们看一下 Kafka 生产者发送消息的代码&#xff1a; private Future<RecordMetadata>…

【数据结构C语言】【入门】【首次万字详细解析】入门阶段数据结构可能用到的C语言知识,一章让你看懂数据结构!!!!!!!

前言&#xff1a;欢迎各位光临本博客&#xff0c;这里小编带你直接手撕入门阶段的数据结构的C语言知识&#xff0c;让你不再看见数据结构就走不动道。文章并不复杂&#xff0c;愿诸君耐其心性&#xff0c;忘却杂尘&#xff0c;道有所长&#xff01;&#xff01;&#xff01;&am…

论文阅读 - MDFEND: Multi-domain Fake News Detection

https://arxiv.org/pdf/2201.00987 目录 ABSTRACT INTRODUCTION 2 RELATED WORK 3 WEIBO21: A NEW DATASET FOR MFND 3.1 Data Collection 3.2 Domain Annotation 4 MDFEND: MULTI-DOMAIN FAKE NEWS DETECTION MODEL 4.1 Representation Extraction 4.2 Domain Gate 4.…

【Stm32】从零建立一个工程

这里我们创建“STM32F103”系列的文件&#xff0c;基于“固件库” 1.固件库获取 https://www.st.com.cn/zh/embedded-software/stm32-standard-peripheral-libraries.html 2.使用Keil创建.uvprojx文件 前提是已经下载好了“芯片对应的固件” 3.复制底层驱动代码 将固件库下的…

【图表如何自动排序】

前提&#xff1a;有这样一个表&#xff1a;第1列为姓名&#xff08;用字母A~I代替&#xff09;&#xff0c;第2列假设为销量 用到的函数&#xff1a; Sort(自动排序的区域&#xff0c;以哪一列为基础来排序&#xff0c;降序or升序&#xff09; Choosecols(选择的区域&#xf…

Spring在不同类型之间也能相互拷贝?

场景还原 日常开发中&#xff0c;我们会定义非常多的实体&#xff0c;例如VO、DTO等&#xff0c;在涉及实体类的相互转换时&#xff0c;常使用Spring提供的BeanUtils.copyProperties&#xff0c;该类虽好&#xff0c;可不能贪用。 这不在使用过程中就遇到一个大坑&#xff0c…

二十、功率放大电路

功率放大电路 1、乙类功率放大器的工作过程以及交越失真; 2、复合三极管的复合规则 3、甲乙类功率放大器的工作原理、自举过程

智谱AI:CogVideoX-2b——视频生成模型的得力工具

智谱AI&#xff1a;CogVideoX-2b——视频生成模型的得力工具 文章目录 CogVideoX 简介——它是什么&#xff1f;CogVideoX 具体部署与实践指南一、创建丹摩实例二、配置环境和依赖三、上传模型与配置文件四、开始运行五、Web UI 演示 CogVideoX 简介——它是什么&#xff1f; …

电线覆盖物检测数据集 气球风筝鸟巢 1300张 voc yol

电线覆盖物检测数据集 气球风筝鸟巢 1300张 voc yol 电线覆盖物检测数据集 数据集描述 该数据集是一个专门用于检测电线及其周围环境中的异物的数据集&#xff0c;旨在帮助研究人员和开发者训练和评估基于深度学习的目标检测模型。数据集涵盖了五种常见的电线覆盖物类型&…