Redis-周阳(17. Redis 分布式锁)学习笔记

news2024/11/15 18:00:33

上一篇 : 16.短信验证码

文章目录

  • 1. 相关面试题
  • 2. Redis 搭建
  • 3. 编码实现分布式锁
    • 3.1 建 Model
    • 3.2 改 POM
    • 3.3 写 YML
    • 3.4 主启动
    • 3.5 业务类
    • 3.6 小测试
  • 4. 上述案例存在问题及修改
    • 4.1 没有加单机版的锁
    • 4.2 分布式部署之后,单机版的锁失效
    • 4.3 出现异常时,无法成功释放锁
    • 4.4 服务宕机,无法成功释放锁
    • 4.5 获取锁和设置过期时间分开,不具备原子性
    • 4.6 误删他人的锁
    • 4.7 判断是不是自己的锁 和 释放锁不是原子的
      • 4.7.1 使用 Redis 事务
      • 4.7.2 使用 LUA 脚本
    • 4.8 缓存在锁释放前过期,释放锁时无限循环
      • 4.8.1 Redisson的使用
      • 4.8.2 释放锁的优化
  • 5. 分布式锁总结

1. 相关面试题

  1. Redis除了拿来做缓存,你还见过基于Redis的什么用法?

  2. Redis做分布式锁的时候有需要注意的问题?

  3. 如果是Redis是单点部署的,会带来什么问题? 那你准备怎么解决单点问题呢?

  4. 集群模式下,比如主从模式,有没有什么问题呢?

  5. 那你简单的介绍一下Redlock吧? 你简历上写redisson,你谈谈

  6. Redis分布式锁如何续期?看门狗知道吗?

2. Redis 搭建

略,看前文
此文侧重点是分布式锁,其他所需服务的搭建就不过多展开了

3. 编码实现分布式锁

  • 使用场景:多个服务在同一时间内同一用户只能有一个请求

3.1 建 Model

搭建两个项目

  • Redis-01
  • Redis-02
    02 直接复制 01 修改端口号

3.2 改 POM

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.oneby</groupId>
    <artifactId>boot_redis01</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-actuator -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-pool2 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

        <!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.1.0</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.13.4</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

3.3 写 YML

server.port=1111

spring.redis.database=0
spring.redis.host=
spring.redis.port=6379
#连接池最大连接数(使用负值表示没有限制)默认8
spring.redis.lettuce.pool.max-active=8
#连接池最大阻塞等待时间(使用负值表示没有限制)默认-1
spring.redis.lettuce.pool.max-wait=-1
#连接池中的最大空闲连接默认8
spring.redis.lettuce.pool.max-idle=8
#连接池中的最小空闲连接默认0
spring.redis.lettuce.pool.min-idle=0

3.4 主启动

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class BootRedis01Application {
    public static void main(String[] args) {
        SpringApplication.run(BootRedis01Application.class);
    }
}

3.5 业务类

  • RedisConfig ,配置类

    @Configuration
    public class RedisConfig {
        @Bean
        public RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory connectionFactory){
            // 新建 RedisTemplate 对象,key 为 String 对象,value 为 Serializable(可序列化的)对象
            RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();
            // key 值使用字符串序列化器
            redisTemplate.setKeySerializer(new StringRedisSerializer());
            // value 值使用 json 序列化器
            redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
            // 传入连接工厂
            redisTemplate.setConnectionFactory(connectionFactory);
            // 返回 redisTemplate 对象
            return redisTemplate;
        }
    }
    
  • GoodController ,业务类

    @RestController
    public class GoodController {
    	// 使用 RedisTemplate 的子类
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
    	// 从配置文件注入端口号配置	
        @Value("${server.port}")
        private String serverPort;
    
        @GetMapping("/buy_goods")
        public String buy_Goods() {
            // 从 redis 中获取商品的剩余数量
            String result = stringRedisTemplate.opsForValue().get("goods:001");
            int goodsNumber = result == null ? 0 : Integer.parseInt(result);
            String retStr = null;
    
            // 商品数量大于零才能出售
            if (goodsNumber > 0) {
                int realNumber = goodsNumber - 1;
                stringRedisTemplate.opsForValue().set("goods:001", realNumber + "");
                retStr = "你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + "\t 服务器端口: " + serverPort;
            } else {
                retStr = "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口: " + serverPort;
            }
            System.out.println(retStr);
            return retStr;
        }
    }
    

3.6 小测试

  • 单机情况下分别测试 Redis-01、02:
  • http://192.168.1.6:1111/buy_goods 和 http://192.168.1.6:2222/buy_goods
    在这里插入图片描述

4. 上述案例存在问题及修改

4.1 没有加单机版的锁

  • 没有加锁,在高并发情况下会出现库存不准的问题
  • 解决
    可以通过加锁解决
    在这里插入图片描述
  • 思考:这里到底是加 Synchronized 还是 ReentrantLock?
    • Synchronized 只有代码块执行完成或者异常才会释放锁,可能会有很多线程等候
    • ReentrantLock 使用 lock.tryLock() 可以在等待一定时间后放弃等待
    • 具体使用哪一个 分情况讨论

4.2 分布式部署之后,单机版的锁失效

  • 分布式部署之后,单机版的锁失效,单机版的锁还是会导致超卖现象,这时就需要需要分布式锁

  • Nginx 的安装部署略

  • nginx.conf 配置文件
    在这里插入图片描述

  • 使用 Nginx 访问 Redis-01、02,手动点击貌似没有出错
    在这里插入图片描述

  • 使用 jmeter 进行压测
    在这里插入图片描述

  • 发现问题
    在这里插入图片描述

  • 解决

    • 使用 Redis 的 SETNX 命令

      Redis具有极高的性能,且其命令对分布式锁支持友好,借助 SET 命令即可实现加锁处理
      The SET command supports a set of options that modify its behavior:

      1. EX seconds – Set the specified expire time, in seconds.
      2. PX milliseconds – Set the specified expire time, in milliseconds.
      3. EXAT timestamp-seconds – Set the specified Unix time at which the key will expire, in seconds.
      4. PXAT timestamp-milliseconds – Set the specified Unix time at which the key will expire, in milliseconds.
      5. NX – Only set the key if it does not already exist.
      6. XX – Only set the key if it already exist.
      7. KEEPTTL – Retain the time to live associated with the key.
      8. GET – Return the old value stored at key, or nil when key did not exist.

    在这里插入图片描述

4.3 出现异常时,无法成功释放锁

  • 如果代码在执行的过程中出现异常,那么就可能无法释放锁
  • 解决:因此必须要在代码层面加上 finally 代码块,保证锁的释放
    在这里插入图片描述

4.4 服务宕机,无法成功释放锁

  • 假设部署了微服务 jar 包的服务器挂了,代码层面根本没有走到 finally 这块,也没办法保证解锁。这个 key 没有被删除,其他微服务就一直抢不到锁,因此我们需要加入一个过期时间限定的 key
  • 解决:
    在这里插入图片描述

4.5 获取锁和设置过期时间分开,不具备原子性

  • 加锁与设置过期时间的操作分开了,假设服务器刚刚执行了加锁操作,然后宕机了,也没办法保证解锁。
  • 解决:
    在这里插入图片描述

4.6 误删他人的锁

  • 线程A抢到了锁,但是中间业务代码执行的时间过长,超过了过期时间,那么 A 的锁将会自动释放掉了
  • 线程B在A释放掉之后抢到锁,继续执行业务代码
  • 此时 A 业务代码执行完成,执行 finally 中的释放锁,当前释放的锁就会是B的锁
    在这里插入图片描述
  • 解决:
    释放锁时,判断当前是不是自己的锁
    在这里插入图片描述

4.7 判断是不是自己的锁 和 释放锁不是原子的

  • 在 finally 代码块中的判断与删除并不是原子操作,假设执行 if 判断的时候,这把锁还是属于当前业务,但是有可能刚执行完 if 判断,这把锁就被其他业务给释放了,还是会出现误删锁的情况
  • 解决:(有两个解决思路)
    1. 官网推荐使用 LUA 脚本

      在这里插入图片描述

    2. 使用 Redis 的事务

4.7.1 使用 Redis 事务

  • Redis 事务介绍

    1. Redis的事务是通过MULTl,EXEC,DISCARD和WATCH这四个命令来完成。
    2. Redis的单个命令都是原子性的,所以这里确保事务性的对象是命令集合。
    3. Redis将命令集合序列化并确保处于一事务的命令集合连续且不被打断的执行。
    4. Redis不支持回滚的操作。
  • 相关命令

    1. MULTI
      • 用于标记事务块的开始。
      • Redis会将后续的命令逐个放入队列中,然后使用EXEC命令原子化地执行这个命令序列。
      • 语法:MULTI
    2. EXEC
      • 在一个事务中执行所有先前放入队列的命令,然后恢复正常的连接状态。
      • 语法:EXEC
    3. DISCARD
      • 清除所有先前在一个事务中放入队列的命令,然后恢复正常的连接状态。
      • 语法:DISCARD
    4. WATCH
      • 当某个事务需要按条件执行时,就要使用这个命令将给定的键设置为受监控的状态
      • 语法:WATCH key[key……]
      • 注:该命令可以实现redis的乐观锁
    5. UNWATCH
      • 清除所有先前为一个事务监控的键。
      • 语法:UNWATCH
  • 演示事务的使用
    在这里插入图片描述
    在这里插入图片描述

  • 将 Redis 事务应用到上面代码中
    在这里插入图片描述

4.7.2 使用 LUA 脚本

  • 使用 lua 脚本可以防止别人动自己的锁

    在这里插入图片描述

  • 执行结果
    在这里插入图片描述

4.8 缓存在锁释放前过期,释放锁时无限循环

  • 因为执行业务的时候可能会调用其他服务,我们并不能保证其他服务的调用时间。如果设置的锁过期了,当前业务还正在执行,那么之前设置的锁就失效了,就有可能出现超卖问题。

  • 因此我们需要确保 redisLock 过期时间大于业务执行时间的问题,即面临如何对 Redis 分布式锁进行续期的问题

  • redis 集群环境下,我们自己写的也不OK,直接上 RedLock 之 Redisson 落地实现

4.8.1 Redisson的使用

  • 在 RedisConfig 配置类中注入 Redisson 对象

    @Configuration
    public class RedisConfig {
    
        @Value("${spring.redis.host}")
        private String redisHost;
    
        @Bean
        public RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory connectionFactory) {
            // 新建 RedisTemplate 对象,key 为 String 对象,value 为 Serializable(可序列化的)对象
            RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();
            // key 值使用字符串序列化器
            redisTemplate.setKeySerializer(new StringRedisSerializer());
            // value 值使用 json 序列化器
            redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
            // 传入连接工厂
            redisTemplate.setConnectionFactory(connectionFactory);
            // 返回 redisTemplate 对象
            return redisTemplate;
        }
    
    	// 注入 Redisson
        @Bean
        public Redisson redisson() {
            Config config = new Config();
            config.useSingleServer().setAddress("redis://" + redisHost + ":6379").setDatabase(0);
            return (Redisson) Redisson.create(config);
        }
    
    }
    
  • 修改代码
    在这里插入图片描述

  • 压测
    在这里插入图片描述

  • 此时就基本解决了超卖的情况

4.8.2 释放锁的优化

  • 上面释放锁时,是直接使用的 redissonLock.unlock()
  • 在超高并发的情况下,有可能会出现当前线程和解锁的线程不是同一个,会报出如下异常
    在这里插入图片描述
  • 为了解决上述异常,释放锁时,最好这样来写
    在这里插入图片描述

5. 分布式锁总结

  1. synchronized 锁:单机版 OK,上 nginx分布式微服务,单机锁就不 OK,
  2. 分布式锁:取消单机锁,上 redis 分布式锁 SETNX
  3. 如果出异常的话,可能无法释放锁, 必须要在 finally 代码块中释放锁
  4. 如果宕机了,部署了微服务代码层面根本没有走到 finally 这块,也没办法保证解锁,因此需要有设置锁的过期时间
  5. 除了增加过期时间之外,还必须要 SETNX 操作和设置过期时间的操作必须为原子性操作
  6. 规定只能自己删除自己的锁,你不能把别人的锁删除了,防止张冠李戴,可使用 lua 脚本或者事务
  7. 判断锁所属业务与删除锁的操作也需要是原子性操作
  8. redis 集群环境下,我们自己写的也不 OK,直接上 RedLock 之 Redisson 落地实现

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

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

相关文章

使用iServer的Web打印功能打印临时图层tempLayersSet资源

作者&#xff1a;Carlo 背景&#xff1a;最近很多小伙伴想要了解如何将临时图层资源打印输出为图片&#xff1f;其实&#xff0c;官网已发布的10.2.1版本以及11.1.0版本的iServer就已经支持该功能&#xff0c;我们可以通过下文的介绍来学习使用。 步骤一、生成一个临时图层资源…

【C生万物】 结构体篇

欢迎来到 Claffic 的博客 &#x1f49e;&#x1f49e;&#x1f49e; &#x1f449; 专栏&#xff1a;《C生万物 | 先来学C》&#x1f448; 前言&#xff1a; 这期继续C语言的学习&#xff0c;进行结构体的讲解&#xff0c;值得注意的是&#xf…

Java版企业电子招标采购系统源代码Spring Boot + 二次开发 + 前后端分离 构建企业电子招采平台之立项流程图

项目说明 随着公司的快速发展&#xff0c;企业人员和经营规模不断壮大&#xff0c;公司对内部招采管理的提升提出了更高的要求。在企业里建立一个公平、公开、公正的采购环境&#xff0c;最大限度控制采购成本至关重要。符合国家电子招投标法律法规及相关规范&#xff0c;以及…

SpringCloud------Eureka集群版整合生产(集群)以及消费(五)

SpringCloud------Eureka集群版整合生产以及消费&#xff08;五&#xff09; 集群避免单点故障的发生。 服务注册&#xff1a; 将服务信息注册进服务中心 服务发现&#xff1a; 从注册中心上获取服务信息 实质&#xff1a;存key服务名&#xff0c;取value调用地址 1.先启动eur…

STM32-IAP基本原理及应用 | ICP、IAP程序下载流程 | 程序执行流程 | 配置IAP到STM32F4xxx

目录 1.串口IAP介绍1.1STM32编程方式1.2STM32系列芯片系统存储器区域1.2.1 STM32F40x/41x1.2.2 STM32F42x/43x 1.3STM32启动模式选择1.4ICP下载流程1.5IAP下载流程1.6一般的程序执行流程1.7加入IAP之后程序运行程序1.8STM32复位后如何跳转到main函数1.9IAP升级应用程序过程 2.A…

复合查询 --- MySQL总结(三)

复合查询 文章目录 复合查询多表查询自连接子查询单行查询多行查询多列子查询合并查询 表内连和外连内连外连 多表查询 前面讲述的关于进行一个表的简单查询和关于时间函数的相关问题&#xff0c;下面要进行复合查询的相关内容。 这里要使用卡笛尔集的概率让两个表融合成为一个…

美国第十次加息 正面临史上最严重的债务危机 经济形势堪忧

美联储周三如期宣布加息25个基点&#xff0c;将联邦基金利率目标区间上调到5%至5.25%之间&#xff0c;这已是美联储自去年3月以来的第十次加息。 FOMC在声明中强调&#xff0c;美国第一季度经济活动温和扩张&#xff0c;近几个月新增就业岗位强劲&#xff0c;失业率依然处于低位…

WxGL应用实例:绘制点云

WxGL附带了几个工具函数&#xff0c;其中read_pcfile用来解析.ply和.pcd格式的点云文件&#xff0c;该函数返回一个PointCloudData类实例&#xff0c;包含以下属性&#xff1a; PointCloudData.ok - 数据是否可用&#xff0c;布尔型PointCloudData.info - 数据可用性说明&…

Vue 面试题汇总

前言 面试题整理自 Vue面试专题&#xff0c;题解结合了个人的思考和理解&#xff0c;供大家参考。 个人觉得村长的题目选的都挺好的&#xff0c;而且题解也很全面&#xff0c;就是题解比较书面&#xff0c;大家实际面试的时候可以尽量口语化&#xff0c;按照答题思路组织语言…

BadUsb使用

1 IDE下载 地址&#xff1a;Software | Arduino 2 开发版驱动安装 linux和mac版本会自动识别提示你安装开发板&#xff0c;驱动貌似不需要额外安装 win需要根据板子型号去下载安装驱动 如 Arduino驱动的安装教程-DFRobot产品资料库 默认会提示你根据你插入的设备进行提示…

C++ 中的引用

引用 变量名&#xff0c;本身是一段内存的引用&#xff0c;即别名(alias).引用可以看作一个已定义变量的别名 引用的语法: Tvpe& name var&#xff1b; 类型& 引用变量名称 变量名称 这就是引用变量的定义。&和类型结合称之为引用符号&#xff0c;不是取…

【一起啃书】《机器学习》第六章支持向量机

文章目录 第六章 支持向量机6.1 间隔和支持向量6.2 对偶问题6.3 核函数6.4 软间隔与正则化6.5 支持向量回归6.6 核方法6.7 一些问题 第六章 支持向量机 6.1 间隔和支持向量 给定训练样本集 D { ( x 1 , y 1 ) , ( x 2 , y 2 ) , . . . , ( x m , y m ) } , y i ∈ { − 1 , …

Echarts 自定义y轴value值

在CSDN问答区&#xff0c;碰到一个有意思的问题&#xff0c;解决思路在这里记录一下。 需求为&#xff1a;进行四等份展示&#xff0c;即中间价差值 (最大值 - 最小值) / 4&#xff0c;左侧数据从上到下分别为&#xff1a;最大值、最大值 - (最大值 - 最小值) / 4、最大值 - …

测试工程师用了3个月从月薪8k涨到12k,我是这么做到的?

先说一下自己的个人情况&#xff0c;大专生&#xff0c;18年通过校招进入湖南金蝶软件公司&#xff0c;干了接近3年的测试工程师&#xff0c;今年年初&#xff0c;感觉自己不能够在这样下去了&#xff0c;长时间呆在一个舒适的环境会让一个人堕落&#xff01; 而我已经在一个企…

MySQL知识学习04(MySQL事务隔离级别详解)

1、事务隔离级别总结&#xff1f; SQL 标准定义了四个隔离级别&#xff1a; READ-UNCOMMITTED(读取未提交) &#xff1a; 最低的隔离级别&#xff0c;允许读取尚未提交的数据变更&#xff0c;可能会导致脏读、幻读或不可重复读。READ-COMMITTED(读取已提交) &#xff1a; 允许…

剧本杀游戏app开发

剧本杀游戏app开发通常会涉及以下技术&#xff1a; 开发语言&#xff1a;剧本杀游戏app可以使用各种编程语言进行开发&#xff0c;例如Java、Kotlin、Swift等。 游戏引擎开发&#xff1a;为了实现游戏过程中的角色扮演、对话、动画等效果&#xff0c;需要使用适当的游戏…

Docker-Compose介绍

文章目录 一、Docker-compose 简介二、YAML 文件格式及编写注意事项三、Docker Compose配置常用字段四、Docker Compose 常用命令五、Docker Compose 文件结构六、compose 部署1、Docker Compose 环境安装2、compose部署nginx3、compose部署lnmp 一、Docker-compose 简介 Dock…

odoo 常用小部件及其用法

文章目录 1) 显示百分比2) 标签组件3) handle组件&#xff08;拖拽排序&#xff09;4) 状态栏组件5) binary组件6) 货币组件7) tatinfo组件8) 日期型字段只显示年月 odoo的一些小部件主要定义在&#xff1a;模块/static/src/js下 1) 显示百分比 模型字段execution_percent定义…

api接口如何有效对接

一、背景 在平时工作中&#xff0c;经常会遇到的一种场景是&#xff1a;A公司要对接B公司的API方法&#xff0c;这时&#xff0c;A公司就要阅读B公司的接口文档&#xff0c;从接口文档中找到自己需要对接的API&#xff0c;并根据接口文档的要求&#xff0c;完成编码工作&#…

为什么要建数据仓库,而不是直连数据源?

各位数据的朋友&#xff0c;大家好&#xff0c;我是老周道数据&#xff0c;和你一起&#xff0c;用常人思维数据分析&#xff0c;通过数据讲故事。 今天和大家聊一个话题&#xff1a;为什么BI软件要用构建数据仓库&#xff0c;而不是直连数据源的方式开发报表&#xff1f;&…