Redis(十八)Redlock算法

news2024/11/14 14:31:01

文章目录

    • 自研锁逻辑
      • lock加锁关键逻辑
    • Redlock红锁算法
      • 自研锁存在的问题
      • Redlock算法设计理念
      • Redison
      • Redisson使用案例
      • Redisson源码分析
      • 多机案例

自研锁逻辑

按照JUC里面java.util.concurrent.locks.Lock接规范编写

lock加锁关键逻辑

  1. 加锁的Lua脚本,通过redis里面的hash数据模型,加锁和可重入性都要保证
  2. 加锁不成,需要while进行重试并自旋
  3. 自动续期,加个钟
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException
{
    if(time == -1L)
    {
    	//第一步
        String script =
                "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then    " +
                        "redis.call('hincrby',KEYS[1],ARGV[1],1)    " +
                        "redis.call('expire',KEYS[1],ARGV[2])    " +
                        "return 1  " +
                "else   " +
                        "return 0 " +
                "end";
        System.out.println("lockName:"+lockName+"\t"+"uuidValue:"+uuidValue);
		//第二步
        while(!stringRedisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class), Arrays.asList(lockName), uuidValue,String.valueOf(expireTime)))
        {
            //暂停60毫秒
            try { TimeUnit.MILLISECONDS.sleep(60); } catch (InterruptedException e) { e.printStackTrace(); }
        }
        //第三步:新建一个后台扫描程序,来坚持key目前的ttl,是否到我们规定的1/2 1/3来实现续期
        renewExpire();
        return true;
    }
    return false;
}

Redlock红锁算法

https://redis.io/docs/manual/patterns/distributed-locks/

自研锁存在的问题

在这里插入图片描述

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

Redlock算法设计理念

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

Redison

https://github.com/redisson/redisson/wiki/8.-distributed-locks-and-synchronizers/#84-redlock

@Autowired
private Redisson redisson;
public String saleByRedisson()
{
    String retMessage = "";

    RLock redissonLock = redisson.getLock("zzyyRedisLock");
    redissonLock.lock();

    try
    {
        //1 查询库存信息
        String result = stringRedisTemplate.opsForValue().get("inventory001");
        //2 判断库存是否足够
        Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
        //3 扣减库存,每次减少一个
        if(inventoryNumber > 0)
        {
            stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
            retMessage = "成功卖出一个商品,库存剩余:"+inventoryNumber;
            System.out.println(retMessage+"\t"+"服务端口号"+port);
        }else{
            retMessage = "商品卖完了,o(╥﹏╥)o";
        }
    }finally {
        //改进点,只能删除属于自己的key,不能删除别人的
        if(redissonLock.isLocked() && redissonLock.isHeldByCurrentThread())
        {
            redissonLock.unlock();
        }
    }
    return retMessage+"\t"+"服务端口号"+port;
}

Redisson使用案例

https://github.com/redisson/redisson#quick-start
YML

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

RedisConfig

//单机
@Configuration
public class RedisConfig
{
    @Bean
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory)
    {
        RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(lettuceConnectionFactory);
        //设置key序列化方式string
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        //设置value的序列化方式json
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());

        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());

        redisTemplate.afterPropertiesSet();

        return redisTemplate;
    }

    //单Redis节点模式
    @Bean
    public Redisson redisson()
    {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.111.175:6379").setDatabase(0).setPassword("111111");
        return (Redisson) Redisson.create(config);
    }
}
@Autowired
private Redisson redisson;
public String saleByRedisson()
{
    String retMessage = "";

    RLock redissonLock = redisson.getLock("zzyyRedisLock");
    redissonLock.lock();

    try
    {
        //1 查询库存信息
        String result = stringRedisTemplate.opsForValue().get("inventory001");
        //2 判断库存是否足够
        Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);
        //3 扣减库存,每次减少一个
        if(inventoryNumber > 0)
        {
            stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
            retMessage = "成功卖出一个商品,库存剩余:"+inventoryNumber;
            System.out.println(retMessage+"\t"+"服务端口号"+port);
        }else{
            retMessage = "商品卖完了,o(╥﹏╥)o";
        }
    }finally {
        //改进点,只能删除属于自己的key,不能删除别人的
        if(redissonLock.isLocked() && redissonLock.isHeldByCurrentThread())
        {
            redissonLock.unlock();
        }
    }
    return retMessage+"\t"+"服务端口号"+port;
}

Redisson源码分析

守护线程续命

分布式锁过期,但业务逻辑没有处理完

在获取锁成功后,给锁加一个watchdog,watchdog会启动一个定时任务,在锁没有被释放且快要过期的时候续期。
源码分析1

Redission创建锁key,默认30秒

在这里插入图片描述
在这里插入图片描述
源码分析2

调用锁方法->AQS尝试获取锁->锁续期

RedissonLock.java
lock()—tryAcquire()—tryAcquireAsync()—

在这里插入图片描述
源码分析3

lua脚本保证原子性

  1. 通过exists判断,如果锁不存在,则设置值和过期时间,加锁成功
  2. 通过hexists判断,如果锁已存在,并且锁的是当前线程,则证明是重入锁,加锁成功
  3. 如果锁已存在,但锁的不是当前线程,则证明有其他线程持有锁。返回当前锁的过期时间(代表了锁key的剩余生存时间),加锁失败

在这里插入图片描述
源码分析4

续期

在这里插入图片描述
这里面初始化了一个定时器,dely 的时间是 internalLockLeaseTime/3。
在 Redisson 中,internalLockLeaseTime 是 30s,也就是每隔 10s 续期一次,每次 30s。
在这里插入图片描述

客户端A加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端A还持有锁key,那么就会不断的延长锁key的生存时间,默认每次续命又从30秒新开始

在这里插入图片描述

多机案例

理论来源官网:
在这里插入图片描述
这个锁的算法实现了多redis实例的情况,相对于单redis节点来说,优点在于防止了单节点故障造成整个服务停止运行的情况且在多节点中锁的设计,及多节点同时崩溃等各种意外情况有自己独特的设计方法。

Redisson 分布式锁支持 MultiLock 机制可以将多个锁合并为一个大锁,对一个大锁进行统一的申请加锁以及释放锁。

最低保证分布式锁的有效性及安全性的要求如下:

  1. 互斥;任何时刻只能有一个client获取锁
  2. 释放死锁;即使锁定资源的服务崩溃或者分区,仍然能释放锁
  3. 容错性;只要多数redis节点(一半以上)在使用,client就可以获取和释放锁

基于故障转移实现的redis主从无法真正实现Redlock:

因为redis在进行主从复制时是异步完成的,比如在clientA获取锁后,主redis复制数据到从redis过程中崩溃了,导致没有复制到从redis中,然后从redis选举出一个升级为主redis,造成新的主redis没有clientA 设置的锁,这是clientB尝试获取锁,并且能够成功获取锁,导致互斥失效;

已弃用This object(RedLock) is deprecated Use RLock or RFencedLock instead.
在这里插入图片描述
在这里插入图片描述
案例

docker run -p 6381:6379 --name redis-master-1 -d redis
docker run -p 6382:6379 --name redis-master-2 -d redis
docker run -p 6383:6379 --name redis-master-3 -d redis
docker exec -it redis-master-1 /bin/bash 或者 docker exec -it redis-master-1 redis-cli

POM

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.19.1</version>
</dependency>
spring.redis.single.address1=192.168.217.129:6381
spring.redis.single.address2=192.168.217.129:6382
spring.redis.single.address3=192.168.217.129:6383

读取配置类

@ConfigurationProperties(prefix = "spring.redis", ignoreUnknownFields = false)
@Data
public class RedisProperties {
    private int database;
    /**
     * 等待节点回复命令的时间。该时间从命令发送成功时开始计时
     */
    private int timeout;
    private String password;
    private String mode;
    /**
     * 池配置
     */
    private RedisPoolProperties pool;
    /**
     * 单机信息配置
     */
    private RedisSingleProperties single;
}
@Data
public class RedisPoolProperties {
    private int maxIdle;
    private int minIdle;
    private int maxActive;
    private int maxWait;
    private int connTimeout;
    private int soTimeout;
    /**
     * 池大小
     */
    private  int size;
}
@Data
public class RedisSingleProperties {
    private  String address1;
    private  String address2;
    private  String address3;
}

配置类

@Configuration
@EnableConfigurationProperties(RedisProperties.class)
public class CacheConfiguration {
    @Autowired
    RedisProperties redisProperties;
    
    @Bean
    RedissonClient redissonClient1() {
        Config config = new Config();
        String node = redisProperties.getSingle().getAddress1();
        node = node.startsWith("redis://") ? node : "redis://" + node;
        SingleServerConfig serverConfig = config.useSingleServer()
                .setAddress(node)
                .setTimeout(redisProperties.getPool().getConnTimeout())
                .setConnectionPoolSize(redisProperties.getPool().getSize())
                .setConnectionMinimumIdleSize(redisProperties.getPool().getMinIdle());
        if (StringUtils.isNotBlank(redisProperties.getPassword())) {
            serverConfig.setPassword(redisProperties.getPassword());
        }
        return Redisson.create(config);
    }
    @Bean
    RedissonClient redissonClient2() {
        Config config = new Config();
        String node = redisProperties.getSingle().getAddress2();
        node = node.startsWith("redis://") ? node : "redis://" + node;
        SingleServerConfig serverConfig = config.useSingleServer()
                .setAddress(node)
                .setTimeout(redisProperties.getPool().getConnTimeout())
                .setConnectionPoolSize(redisProperties.getPool().getSize())
                .setConnectionMinimumIdleSize(redisProperties.getPool().getMinIdle());
        if (StringUtils.isNotBlank(redisProperties.getPassword())) {
            serverConfig.setPassword(redisProperties.getPassword());
        }
        return Redisson.create(config);
    }

    @Bean
    RedissonClient redissonClient3() {
        Config config = new Config();
        String node = redisProperties.getSingle().getAddress3();
        node = node.startsWith("redis://") ? node : "redis://" + node;
        SingleServerConfig serverConfig = config.useSingleServer()
                .setAddress(node)
                .setTimeout(redisProperties.getPool().getConnTimeout())
                .setConnectionPoolSize(redisProperties.getPool().getSize())
                .setConnectionMinimumIdleSize(redisProperties.getPool().getMinIdle());
        if (StringUtils.isNotBlank(redisProperties.getPassword())) {
            serverConfig.setPassword(redisProperties.getPassword());
        }
        return Redisson.create(config);
    }

    /**
     * 单机
     * @return
     */
    /*@Bean
    public Redisson redisson()
    {
        Config config = new Config();

        config.useSingleServer().setAddress("redis://192.168.111.147:6379").setDatabase(0);

        return (Redisson) Redisson.create(config);
    }*/
}

controller

@RestController
@Slf4j
public class RedLockController {

    public static final String CACHE_KEY_REDLOCK = "ATGUIGU_REDLOCK";

    @Autowired
    RedissonClient redissonClient1;

    @Autowired
    RedissonClient redissonClient2;

    @Autowired
    RedissonClient redissonClient3;

    boolean isLockBoolean;

    @GetMapping(value = "/multiLock")
    public String getMultiLock() throws InterruptedException
    {
        String uuid =  IdUtil.simpleUUID();
        String uuidValue = uuid+":"+Thread.currentThread().getId();

        RLock lock1 = redissonClient1.getLock(CACHE_KEY_REDLOCK);
        RLock lock2 = redissonClient2.getLock(CACHE_KEY_REDLOCK);
        RLock lock3 = redissonClient3.getLock(CACHE_KEY_REDLOCK);

        RedissonMultiLock redLock = new RedissonMultiLock(lock1, lock2, lock3);
        redLock.lock();
        try
        {
            System.out.println(uuidValue+"\t"+"---come in biz multiLock");
            try { TimeUnit.SECONDS.sleep(30); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println(uuidValue+"\t"+"---task is over multiLock");
        } catch (Exception e) {
            e.printStackTrace();
            log.error("multiLock exception ",e);
        } finally {
            redLock.unlock();
            log.info("释放分布式锁成功key:{}", CACHE_KEY_REDLOCK);
        }

        return "multiLock task is over  "+uuidValue;
    }

}

自动续期
在这里插入图片描述

命令

ttI ATGUIGU_REDLOCK
HGETALL ATGUIGU_REDLOCK
shutdown
docker start redis-master-1
docker exec -it redis-master-1 redis-cli

在这里插入图片描述

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

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

相关文章

vue3+vite - 报错 import.meta.glob() can only accept string literals.(详细解决方案)

报错说明 在vue3+vite项目中,解决报错: [plugin:vite:import-analysis] import.meta.glob() can only accept string literals. 如果我们报错差不多,就可以完美搞定这个错误。 解决教程 这个错误,是因为

【笔记】MJ Prompt

参数 --chaos 10 or --c 10, 0-10, defalut 0 --quality 1 or --q, 0.25-1, defalut 1 --iw 2, 0.5-2, --stylize 100 or --s 100, 0-1000, defalut 100 --cref URL --cw 100, 0-100stylize 风格化&#xff0c;MJ不同的出图模式&#xff0c;有默认的艺术风格&#xff0c;该值…

TCP协议中的传输控制机制图文详解「重传机制」「流量控制」「拥塞控制」

目录 TCP重传机制 超时重传 快速重传 SACK 方法 Duplicate SACK TCP 流量控制 滑动窗口 累积确认 窗口大小由哪一方决定&#xff1f; 接收窗口和发送窗口的大小是相等的吗&#xff1f; 流量控制 窗口关闭的后果 糊涂窗口综合症 TCP拥塞处理 为什么要有拥塞控制呀&#xff0c;不…

LinkedIn账号为什么被封?被封后如何解决?

近期会有一些小伙伴说自己遇到了帐号无法登录的情况&#xff0c;其实出现领英帐号被封号(被限制登录)主要会有两类情况&#xff0c;今天就给大家分享一下如果被封该如何解决&#xff0c;强烈建议收藏。 在电脑领英官网或者手机领英APP上&#xff0c;输入领英帐号密码点击登录后…

力扣● 84.柱状图中最大的矩形

84.柱状图中最大的矩形 需要找到元素i的上一个更小的元素leftmin和下一个更小的元素rightmin&#xff0c;这样leftmin和rightmin之间的元素都比当前元素i更大&#xff0c;那么矩形的宽就是中间的这些元素&#xff1a;可以从leftmin1延伸到rightmin-1&#xff0c;长即为height[i…

rancher2.6部署

rancher2.6部署 1、准备环境镜像 2、部署3、密码获取密码设置新密码 4、设置语言5、导入已有集群 1、准备 环境 docker-ce-20.10.23-3.el8.x86_64.rpm以及依赖rpm kubernetes&#xff1a;v1.23.17 镜像 &#xff08;rancher和k8s有个版本对应关系&#xff0c;rancher2.5就不…

走进redisson

这里作者将大家走进redisson&#xff0c;读完这篇相信加深你对redisson的获取锁&#xff0c;重入&#xff0c;超时&#xff0c;看门狗&#xff0c;发布订阅等原理和功能的理解。 本文将深入原理代码&#xff0c;给出每行代码的意义以及最后的效果&#xff0c;过程有些枯燥&…

Python 指南-最短路径(Dijkstra 算法):

Dijkstra 算法可在 Python 库 OSMNX 中实现&#xff0c;可用于查找两个位置之间按距离或时间加权的最短路径。该算法使用 OpenStreetMap (OSM) 网络来驾驶、步行或骑自行车&#xff0c;并在后台使用 Python 库 NETWORKX 查找路线。 编码练习 正如我提到的&#xff0c;我将做一…

MySQL数据库 @@transaction_isolation参数的查询及修改

在应用开发过程中&#xff0c;可能会检查mysql数据库初始化参数符合要求。 遇到这种情况就要进行相应的调整。 1、查询参数信息 select transaction_isolation; 2、找到配置文件 &#xff0c;以window系统为例。 修改前先关闭MySQL数据库服务 对应需要修改的参数&#xff…

C语言看完我这篇最详细文件操作,你不会也得会!!!

1.使用文件 我们写的程序的数据是存储在电脑内存中&#xff0c;如果程序退出&#xff0c;内存就会被回收&#xff0c;数据就丢失&#xff0c;内存更具有一些实时性&#xff0c;等再次运行程序的数据的&#xff0c;数据就消失了&#xff0c;如果想要持久化的保存&#xff0c;可以…

LangChain-Chatchat

文章目录 关于 LangChain-Chatchat特性说明实现原理文档处理流程技术路线图&#xff08;截止0.2.10&#xff09; 使用 关于 LangChain-Chatchat Langchain-Chatchat&#xff08;原Langchain-ChatGLM&#xff09;基于 Langchain 与 ChatGLM 等语言模型的本地知识库问答。 gith…

[自研开源] 数据集成之分批传输 v0.7

开源地址&#xff1a;gitee | github 详细介绍&#xff1a;MyData 基于 Web API 的数据集成平台 部署文档&#xff1a;用 Docker 部署 MyData 使用手册&#xff1a;MyData 使用手册 试用体验&#xff1a;https://demo.mydata.work 交流Q群&#xff1a;430089673 介绍 本篇基于…

STM32F4x7标准库移植LWIP

项目背景 使用GD芯片的我们&#xff0c;都会去参考ST的代码。可是呢&#xff0c;有一个很大的问题就是&#xff0c;ST早就提供HAL库了&#xff0c;而目前GD还只有标准库。在移植LWIP的时候&#xff0c;会有很多不便。 好在天无绝人之路&#xff0c;找到了一份ST的官方例程&am…

java常用IO流功能——字符流和缓冲流概述

前言&#xff1a; 整理下学习笔记&#xff0c;打好基础&#xff0c;daydayup! 之前说了下了IO流的概念&#xff0c;并整理了字节流&#xff0c;有需要的可以看这篇 java常用应用程序编程接口&#xff08;API&#xff09;——IO流概述及字节流的使用 字符流 FileReader(文件字…

基于注意力机制和损坏特征掩蔽的遮挡人脸识别

Occluded Face Recognition Based on Attention Mechanism and Damaged Feature Masking 摘要 本文提出了一种基于注意力机制&#xff08;BAM&#xff09;和掩模生成器的新型遮挡人脸识别方法。在主干网络中嵌入BAM以提取更多可区分的特征&#xff0c;同时设计掩模生成器来清理…

媒介盒子揭秘0基础写好软文的秘诀

在流量见顶的传播环境下&#xff0c;品牌获取用户注意力的主要方式就在软文&#xff0c;然而有许多品牌在写软文时往往摸不着头脑&#xff0c;不知道怎么写&#xff0c;如何写才能使品牌有效曝光&#xff0c;今天媒介盒子就来和大家聊聊&#xff1a;0基础也能写好软文的秘诀&am…

SAP BAS中Fiori开发的高阶功能(storyboard, navigation, guided development, variant)

1. 前言 在之前的几篇文章中&#xff0c;我介绍了SAP BAS的一些基本功能&#xff0c;包括账户申请&#xff0c;创建工作区&#xff0c;git的使用以及如何step-by-step去创建出你的第一个Fiori项目等等。在本篇中&#xff0c;我将进一步介绍一些在开发Fiori应用程序时会用到的高…

【数仓】DataX软件安装及配置,从mysql同步到hdfs

相关文章 【数仓】基本概念、知识普及、核心技术【数仓】数据分层概念以及相关逻辑【数仓】Hadoop软件安装及使用&#xff08;集群配置&#xff09;【数仓】Hadoop集群配置常用参数说明【数仓】zookeeper软件安装及集群配置【数仓】kafka软件安装及集群配置【数仓】flume软件安…

C语言例4-7:格式字符f的使用例子

%f&#xff0c;实型&#xff0c;小数部分为6位 代码如下&#xff1a; //格式字符f的使用例子 #include<stdio.h> int main(void) {float f 123.456;double d1, d2;d11111111111111.111111111;d22222222222222.222222222;printf("%f,%12f,%12.2f,%-12.2f,%.2f\n&qu…