分布式锁系列之Redis分布式锁

目录

介绍

模拟订单超卖场景

代码版

不加锁情况

synchronized加锁

 ​编辑

lock加锁

整合Mysql版

不加锁版 

 synchronized加锁

 lock加锁版

 jvm加锁失效情况

多例模式

事务

 集群搭建

书写sql解决集群超卖

使用悲观锁select ...for update

​编辑 不加悲观锁情况

使用for update加悲观锁

整合项目使用悲观锁

悲观锁的问题

使用乐观锁version

实现乐观锁

乐观锁存在的问题

Mysql锁总结

Redis

使用redis模拟超卖场景

 redis乐观锁实现

redis分布式锁

简单分布式锁

Lua脚本简单使用

初步使用lua

锁可重入性问题 

锁自动续期 

 小总结

 Redisson分布式锁

介绍

简单使用可重入锁

公平锁

联锁(只做了解)

 红锁(只做了解)​编辑

 读写锁

RSemaphore信号量

RCountDownLatch倒计数器

Redisson小结


介绍

分布式锁是一种用于在分布式系统中实现互斥访问的机制。在多个节点同时访问共享资源时,分布式锁可以确保只有一个节点能够获得对资源的独占访问权,从而避免数据竞争和冲突

模拟订单超卖场景

代码版

以现实中线上抢购订单来作为模拟场景测试

假设仓库中有1000件库存

分布式锁系列之Redis分布式锁_第1张图片

不加锁情况

 服务层

分布式锁系列之Redis分布式锁_第2张图片

控制层

 分布式锁系列之Redis分布式锁_第3张图片

 使用apipost进行压测,设置线程数为10,执行次数为100,理想情况最后的库存量应为0

分布式锁系列之Redis分布式锁_第4张图片

测试:

 分布式锁系列之Redis分布式锁_第5张图片

 分布式锁系列之Redis分布式锁_第6张图片

发现库存有重复出现库存余量的情况,并且最后的库存余量也没有为0,这说明有多个线程同时抢到同一个订单时库存并没有正常削减,这样的情况就会导致最后的库存余量错误,但是实际上库存已被卖光,从而出现超卖现象

synchronized加锁

针对这种情况可以使用jvm的synchronized加锁来处理下

分布式锁系列之Redis分布式锁_第7张图片

 重新测试:

分布式锁系列之Redis分布式锁_第8张图片

 分布式锁系列之Redis分布式锁_第9张图片

发现库存余量这次都有序递减,并且最后库存余量正常为0

lock加锁

除了synchronized 还可以使用lock进行加锁

分布式锁系列之Redis分布式锁_第10张图片

 测试:

分布式锁系列之Redis分布式锁_第11张图片

 分布式锁系列之Redis分布式锁_第12张图片

lock加锁和synchronized加锁最后情况都是一致的,库存余量都正常递减并且都符合理论的结果

接下来看下三者的压测结果对比

传统不加锁压测结果:

分布式锁系列之Redis分布式锁_第13张图片

 synchronized加锁压测结果:

分布式锁系列之Redis分布式锁_第14张图片

 lock加锁压测情况:

分布式锁系列之Redis分布式锁_第15张图片

三者纵向对比发现不加锁情况下请求时间要比加锁的两种情况要快,但是安全性不好,会出现超卖情况. synchronized和lock加锁虽然请求时间上要有一些牺牲但是安全性可以得到保证,而且synchronized性能要略比lock好一些

整合Mysql版

创建商品表

分布式锁系列之Redis分布式锁_第16张图片

插入测试数据

分布式锁系列之Redis分布式锁_第17张图片 对应实体类

@Data
@EqualsAndHashCode(callSuper = false)
@TableName("t_goods")
@Accessors(chain = true)
public class Goods implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 商品id
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    /**
     * 商品名称
     */
    private String goodsName;

    /**
     * 商品标题
     */
    private String goodsTitle;

    /**
     * 商品图片
     */
    private String goodsImg;

    /**
     * 商品详情
     */
    private String goodsDetail;

    /**
     * 商品价格
     */
    private BigDecimal goodsPrice;

    /**
     * 商品库存 -1表示没有限制
     */
    private Integer goodsStock;


}

不加锁版 

服务层

分布式锁系列之Redis分布式锁_第18张图片

 启动测试:

最终idea日志

分布式锁系列之Redis分布式锁_第19张图片

 数据库数据:

分布式锁系列之Redis分布式锁_第20张图片

 两者一致,都没有达到预期0结果

压测日志:

分布式锁系列之Redis分布式锁_第21张图片

 synchronized加锁

将数据库库存改回1000重新加锁测试

分布式锁系列之Redis分布式锁_第22张图片

重新压测:

 分布式锁系列之Redis分布式锁_第23张图片

分布式锁系列之Redis分布式锁_第24张图片 结果符合预期

压测日志

分布式锁系列之Redis分布式锁_第25张图片

 lock加锁版

记得库存调回1000

分布式锁系列之Redis分布式锁_第26张图片

测试:

分布式锁系列之Redis分布式锁_第27张图片

分布式锁系列之Redis分布式锁_第28张图片

压测日志:

 分布式锁系列之Redis分布式锁_第29张图片

 jvm加锁失效情况

多例模式

注意前面的场景中使用的都是单例模式,如果是多例模式的话,jvm锁的情况又会发生什么的变化呢

额外补充:

在Spring Boot中,@Scope注解用于指定Bean的作用域范围。Bean的作用域定义了Bean的生命周期和可见性。

@Scope注解可以应用于类级别或方法级别,用于控制Bean的创建和销毁。

以下是@Scope注解的常用取值和用法:

  1. singleton(默认值):每个Spring容器只创建一个Bean实例。这意味着所有使用该Bean的地方都引用的是同一个实例。

  2. prototype:每次通过容器获取Bean时都会创建一个新的实例。这意味着每次获取Bean时都会重新创建一个新的对象。

  3. request:每个HTTP请求都创建一个新的Bean实例。通常用于Web应用程序中的控制器和部分服务。

  4. session:每个用户会话(Session)为其创建一个Bean实例。同一个用户会话中获取到的Bean是同一个实例。

  5. global session:在portlet环境下,每个Portlet全局会话(Global Session)为其创建一个Bean实例。只在基于portlet的web应用中使用。

 下面调整服务层为多例模式

lock版

分布式锁系列之Redis分布式锁_第30张图片

重新测试:

分布式锁系列之Redis分布式锁_第31张图片 分布式锁系列之Redis分布式锁_第32张图片

synchronized版

分布式锁系列之Redis分布式锁_第33张图片

 分布式锁系列之Redis分布式锁_第34张图片

 分布式锁系列之Redis分布式锁_第35张图片

 现在测试结果发现多例模式下无论是lock和synchronized都失效了,库存数据都没有符合理想的0结果

事务

在添加事务时也会造成jvm锁的失效

添加默认事务,可重复读

分布式锁系列之Redis分布式锁_第36张图片

 测试

分布式锁系列之Redis分布式锁_第37张图片

分布式锁系列之Redis分布式锁_第38张图片

设置事务隔离级别为读未提交然后再测试:

分布式锁系列之Redis分布式锁_第39张图片

分布式锁系列之Redis分布式锁_第40张图片

分布式锁系列之Redis分布式锁_第41张图片

测试发现如果事务隔离级别为未提交的话仓库库存是符合预期的 

但是这种读未提交的事务级别是不推荐使用的,原因如下:

事务的隔离级别是数据库提供的一种机制,用于控制在并发环境下事务之间的隔离程度。读未提交(Read Uncommitted)是最低的隔离级别,其特点是一个事务可以读取另一个事务未提交的数据。虽然在某些特定场景下读未提交可以解决超卖问题,但并不推荐将事务级别设置为读未提交,原因如下:

  1. 脏读问题:读未提交级别允许事务读取未提交的数据,但这意味着可能读到不完整或临时的数据。如果其他事务回滚了刚刚提交的数据,那么读取到的数据可能是不一致的,导致脏读问题。

  2. 不可重复读问题:不可重复读是指在同一个事务中,多次读取同一行数据得到不同的结果。读未提交级别下,一个事务可能在读取数据的过程中,另一个事务修改了这个数据,导致两次读取的结果不一致。这会破坏数据的一致性。

  3. 幻读问题:幻读是指在同一个事务中,多次查询同一个范围的数据,得到不同数量的结果。读未提交级别下,一个事务可能在读取数据的过程中,另一个事务插入了新的数据,导致查询的结果集发生变化。这也是数据的一致性问题。

  4. 并发冲突问题:使用读未提交级别会增大事务之间的并发冲突几率,因为不同事务同时读取和修改同一份数据的风险更大。这可能导致更多的等待和冲突,进而影响系统性能和并发能力。

因此,虽然读未提交级别可以一定程度上解决超卖问题,但它会引入更多的一致性和并发冲突问题。为了保证数据的一致性和可靠性,同时提高系统性能和并发能力,通常推荐将事务级别设置为更高的隔离级别,例如读已提交(Read Committed)或可重复读(Repeatable Read),并综合使用悲观锁、乐观锁等机制来解决超卖问题。

 集群搭建

当服务搭建成集群模式时也会使jvm锁机制失效 而且集群模式和多例模式有点类似 在单个服务时,服务为单例模式,但是在集群中的话每个集群都是一个单例,每个集群的单例互相之间并不相同,从而集群也可看为多例模式  

首先把服务层改回单例加锁生效的版本

分布式锁系列之Redis分布式锁_第42张图片

 在idea中复制一个服务实例启动,模拟集群搭建

分布式锁系列之Redis分布式锁_第43张图片

 分布式锁系列之Redis分布式锁_第44张图片

 启动两个实例

分布式锁系列之Redis分布式锁_第45张图片

由于是集群搭建,所以就搭建两个服务,这里使用nginx做服务代理

配置nginx:

 分布式锁系列之Redis分布式锁_第46张图片

 分布式锁系列之Redis分布式锁_第47张图片

保存启动nginx,注意nginx所在文件路径下不要包含中文

 分布式锁系列之Redis分布式锁_第48张图片

访问localhost:9001查看nginx是否启动成功

分布式锁系列之Redis分布式锁_第49张图片

说明启动成功

 测试nginx查看是否代理成功,注意这里请求的是nginx的地址

分布式锁系列之Redis分布式锁_第50张图片

 分布式锁系列之Redis分布式锁_第51张图片

分布式锁系列之Redis分布式锁_第52张图片 由nginx代理到本地的集群服务

把数据库库存调整为1000,开始压测

分布式锁系列之Redis分布式锁_第53张图片

分布式锁系列之Redis分布式锁_第54张图片 两台服务器最后的日志都是472

查看数据库:

分布式锁系列之Redis分布式锁_第55张图片

压测结果

 由此在集群模式下,jvm的锁机制也失效了

书写sql解决集群超卖

可以通过书写sql来解决下超卖情况

持久层

分布式锁系列之Redis分布式锁_第56张图片

 服务层 分布式锁系列之Redis分布式锁_第57张图片

启动两个服务实例,还是访问nginx地址,进行压测

分布式锁系列之Redis分布式锁_第58张图片

 分布式锁系列之Redis分布式锁_第59张图片

分布式锁系列之Redis分布式锁_第60张图片 两个集群服务都显示最后的库存为0和数据库的库存一致都达到了理想的结果0

 压测结果

分布式锁系列之Redis分布式锁_第61张图片

使用sql语句可以解决上述三种情况的锁失效问题,但是也会产生新的问题

1. 锁的范围问题 

使用sql进行更改其锁范围是表级锁,并不是行级锁  下面进行验证:

初始化数据

分布式锁系列之Redis分布式锁_第62张图片

 开启事务分布式锁系列之Redis分布式锁_第63张图片

 更改

分布式锁系列之Redis分布式锁_第64张图片

查看数据有无变化

分布式锁系列之Redis分布式锁_第65张图片

没有变化

再更新华为P30

分布式锁系列之Redis分布式锁_第66张图片 分布式锁系列之Redis分布式锁_第67张图片

 还是没变化

提交

分布式锁系列之Redis分布式锁_第68张图片

 查看数据变化

分布式锁系列之Redis分布式锁_第69张图片

这个时候数据才有变化

mysql悲观锁中使用行级锁:

 (1) 锁的查询或者更新条件必须是索引字段

 (2) 查询或者更新条件必须是具体值

2.同一个商品有多条库存记录

之前的数据都是以id为条件进行修改的,如果更换成商品名字或者商品id,进行修改,并且商品在不同仓库都有库存

分布式锁系列之Redis分布式锁_第70张图片

 这个时候sql语句影响的就是两个仓库的库存

3. 无法记录库存变化前后的状态

使用悲观锁select ...for update

mysql的悲观锁 for update 就是行级锁机制

分布式锁系列之Redis分布式锁_第71张图片 不加悲观锁情况

开启事务

分布式锁系列之Redis分布式锁_第72张图片

 模拟并发,左边服务器查询inphone13商品数据,右边服务器同时并发对iphone13进行修改,观察记录变化

分布式锁系列之Redis分布式锁_第73张图片

 分布式锁系列之Redis分布式锁_第74张图片

 注意这个时候就出现问题了,因为两边的查询和修改操作是同时的,所以理想情况应该是左边查询到的应该是右边修改后的数据,也就是库存应为999,但是这时左边查询到的还是修改前的数据,这个时候就出现了幻读的情况  所以理想情况应该是在左边的事务未提交之前,右边的修改不能生效,在左边事务提交之后右边操作才可以执行修改

使用for update加悲观锁

先将库存调整回1000再进行测试

左边重新开启事务,并加锁进行查询,右边并发同时进行修改库存

分布式锁系列之Redis分布式锁_第75张图片

可以看到这次右边的修改语句并没有直接输出Query Ok的日志,光标一直停在等待区,说明左边事务未提交之前,右边修改不会生效  数据也没有变化

分布式锁系列之Redis分布式锁_第76张图片

左边事务进行提交

分布式锁系列之Redis分布式锁_第77张图片

 左边事务提交之后右边立马打印Query ok,说明悲观锁机制已起效果

此时再查询数据  数据变化正常符合预期

分布式锁系列之Redis分布式锁_第78张图片

整合项目使用悲观锁

修改mapper层

分布式锁系列之Redis分布式锁_第79张图片

服务层

分布式锁系列之Redis分布式锁_第80张图片

控制层不变,只是 调用下服务层

iphone13数量改为1000

分布式锁系列之Redis分布式锁_第81张图片

启动两个服务,开始压测

分布式锁系列之Redis分布式锁_第82张图片

 仓库结果 第一个仓库符合预期为0

分布式锁系列之Redis分布式锁_第83张图片

 压测日志

分布式锁系列之Redis分布式锁_第84张图片

下面再测试一下库存不足1000时的情况,将第一个仓库库存改为900

分布式锁系列之Redis分布式锁_第85张图片

再次压测

分布式锁系列之Redis分布式锁_第86张图片 结果为0并没有发生超卖情况 

悲观锁的问题

(1) 性能问题 加了悲观锁后性能上会受到一定程度的影响

(2) 死锁问题: 对多条数据加锁时,加锁顺序要一致 不然会产生死锁问题

左右两侧同时锁数据 左侧锁id为1数据,右侧锁id为2数据

分布式锁系列之Redis分布式锁_第87张图片

 左侧再去获取id为2的锁

分布式锁系列之Redis分布式锁_第88张图片

发现光标一直在等待

此时右侧再去获取id为1的锁

 分布式锁系列之Redis分布式锁_第89张图片

 报错死锁

(3) 库存操作要一致

分布式锁系列之Redis分布式锁_第90张图片

 这样也会出问题

使用乐观锁version

实现乐观锁

在数据库中添加version字段

分布式锁系列之Redis分布式锁_第91张图片

服务层

分布式锁系列之Redis分布式锁_第92张图片

重启服务更改库存为600后开始压测 

分布式锁系列之Redis分布式锁_第93张图片

分布式锁系列之Redis分布式锁_第94张图片

测试结果

 分布式锁系列之Redis分布式锁_第95张图片

 库存变为0,且版本号更改了600次,符合预期库存为0的情况

压测日志

分布式锁系列之Redis分布式锁_第96张图片

 可以看到压测日志中还有失败的请求数

乐观锁存在的问题

(1) 高并发情况下 性能极地

(2) 读写分离情况下 乐观锁并不可靠

Mysql锁总结

 性能:一个sql>悲观锁>ivm锁>乐观锁

如果追求极致性能、业务场景简单并且不需要记录数据前后变化的情况下。 优先选择:一个sql

如果写并发量较低(多读),争抢不是很激烈的情况下优先选择:乐观锁

如果写并发量较高,一般会经常冲突,此时选择乐观锁的话,会导致业务代码不间断的重试。

优先选择:mysql悲观锁

不推荐jvm本地锁。

Redis

使用redis模拟超卖场景

启动redis,设置库存1000

分布式锁系列之Redis分布式锁_第97张图片

添加redsi所需依赖


  org.springframework.boot
  spring-boot-starter-data-redis

编写服务层

分布式锁系列之Redis分布式锁_第98张图片

启动服务开始压测

分布式锁系列之Redis分布式锁_第99张图片

查询redis库存

分布式锁系列之Redis分布式锁_第100张图片

发现并没有变成预取库存为0

压测日志也没有失败的请求

分布式锁系列之Redis分布式锁_第101张图片

 redis乐观锁实现

watch: 可以监控一个或者多个key的值 如果在事务(exec)执行之前 key的值发生变化则取消事务执行

multi: 开启事务

exec: 执行事务

服务层

分布式锁系列之Redis分布式锁_第102张图片

将redis库存重新设置为1000

分布式锁系列之Redis分布式锁_第103张图片

开始压测

分布式锁系列之Redis分布式锁_第104张图片

 查看redis库存

分布式锁系列之Redis分布式锁_第105张图片

 库存变为0符合预期结果

查看压测日志

分布式锁系列之Redis分布式锁_第106张图片

 redis乐观锁性能问题比较差,一般在开发中可以使用redis的分布式锁来进行问题的解决

redis分布式锁

简单分布式锁

分布式锁特征1: 独占排他使用 setnx

操作: 加锁:setnx-> 解锁 :del -> 重试:递归 循环

SETNX命令是Redis中的一条原子性命令,用于给指定的键设置对应的值,但只有在键不存在的情况下才能设置成功。如果键已经存在,则不进行任何操作,并返回0。 语法:SETNX key value 参数: key:要设置的键名。 value:要设置的键值。 返回值: 当设置成功时,返回1。 当键已经存在时,返回0。 SETNX命令常用于实现分布式锁,可以确保在并发环境下只有一个客户端能够获得锁。 示例: 假设我们需要实现一个简单的分布式锁,用于控制某个关键操作的并发访问。我们可以使用SETNX命令来实现:

SETNX lock_key 1

 如果lock_key不存在,即没有其他客户端持有锁,那么这个命令会将lock_key设置为1,并返回1,表示设置成功。否则,如果lock_key已经存在,命令不进行任何操作,直接返回0,表示设置失败。

使用完锁后,可以使用DEL命令来删除锁:

DEL lock_key

 分布式锁系列之Redis分布式锁_第107张图片

可以看到使用setnx设置了两次,只有第一次成功了,说明setnx只有在key不存在时才会设值成功

 分布式锁系列之Redis分布式锁_第108张图片

结合项目

服务层

分布式锁系列之Redis分布式锁_第109张图片

将redis中库存调整为1000

分布式锁系列之Redis分布式锁_第110张图片

启动两个服务进行压测

 分布式锁系列之Redis分布式锁_第111张图片

分布式锁系列之Redis分布式锁_第112张图片

分布式锁系列之Redis分布式锁_第113张图片

压测日志

分布式锁系列之Redis分布式锁_第114张图片 刚才示例中使用的是递归调用,一定程度上性能会有问题,可以调整为循环调用

 分布式锁系列之Redis分布式锁_第115张图片

库存记得调整,重启后压测

压测日志

分布式锁系列之Redis分布式锁_第116张图片分布式锁特征2

防死锁发生
如果redis客户端程序从redis服务中获取到锁之后立马宕机.
解决:给锁添加过期时间。expire 

分布式特征 3

原子性: 获取锁和过期时间: set key value ex 3 nx

现在的锁机制还是有一定的问题,比如setnx刚加了锁后,服务器突然宕机,此时锁还未来得及释放,就会产生死锁问题

在代码中可以直接设置过期时间参数

分布式锁系列之Redis分布式锁_第117张图片

分布式锁特征4

防误删

 此时虽然加了锁过期时间解决了死锁问题,但是也是由于加了过期时间,如果在高并发场景下,过期时间为3秒.但是业务需要5秒甚至更多的执行时间,此时第一个线程需要执行5秒,但是在第三秒的时候锁已经自动过期从而相当于自动释放解锁,那么第二个线程就会得到锁从而进行执行,而第二个线程又需要5秒的执行,但是第一个线程5秒执行完后又会有一个finally块一定会去解锁,此时第二个线程的锁就会被第一个线程解掉,后续线程也都是如此,都会重复第一个线程和第二个线程之间的问题,从而造成锁的误删问题,所以这里还需要为每一个锁添加一个唯一标识,从而告诉每个线程应该删哪个锁 可以使用uuid来作为锁的唯一标识

 代码改造

分布式锁系列之Redis分布式锁_第118张图片

上述改动虽然可以使用uuid来解决锁之间的误删问题,但是在高并发场景下,由于判断线程锁是否是自己的锁和释放锁这两个动作并不是原子性的,所以这里还是会可能产生误删问题,所以必须保证判断锁和释放锁两个动作的原子性

可以使用lua脚本

Lua脚本简单使用

初步使用lua

一次性发送多个指令给redis。redis单线程执行指令遵守one-by-one规则

EVAL script numkeys key [key ...] arg [arg ...]

script : lua 脚本字符串

numkeys: key列表的元素数量

key列表: 以空格分隔 keys[idnex] 从1开始

arg列表: 以空格分隔 arg[index] 从1开始

参考菜鸟教程lua文档

分布式锁系列之Redis分布式锁_第119张图片

 分布式锁系列之Redis分布式锁_第120张图片

注意 eval并不是打印 print('文本') 里的内容,而是打印的函数返回值,所以第二句return时才会打印'hello world!'

分布式锁系列之Redis分布式锁_第121张图片

 分布式锁系列之Redis分布式锁_第122张图片

注意redis中的lua不允许定义全局变量,可以直接定义局部变量进行使用 

lua脚本流程控制

分布式锁系列之Redis分布式锁_第123张图片

 分支控制格式:

if 条件

then 

  代码块

elseif 条件

then

  代码块

else

  代码块

end

演示:

如果5大于7 打印5,否则打印7

分布式锁系列之Redis分布式锁_第124张图片

分布式锁系列之Redis分布式锁_第125张图片

 lua脚本可以通过 redis.call命令来执行redis的指令 

举例:

注意原redis命令的顺序要一致 

分布式锁系列之Redis分布式锁_第126张图片

 分布式锁系列之Redis分布式锁_第127张图片

测试:

千万注意KEYS和ARGV动态传参时两者取值时都是大写,不要小写,不要问为什么,好奇心比较大可以自己试试踩坑

 代码修改:

public void deduct() {
        //加锁 setnx
        //重试 循环调用
        String uuid = UUID.randomUUID().toString();
        while(!strRedisTemplate.opsForValue().setIfAbsent("lock",uuid,3, TimeUnit.SECONDS)){
            try {
                Thread.sleep(50);
                this.deduct();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        try{
            //查询库存信息
            String stock = strRedisTemplate.opsForValue().get("stock").toString();
            log.info("当前库存是:"+stock);
            //判断库存是否充足
            if(stock!=null && stock.length()!=0){
                Integer st = Integer.valueOf(stock);
                if(st>0){
                    //扣减库存
                    strRedisTemplate.opsForValue().set("stock",String.valueOf(--st));
                }
            }
        }finally {
            //判断 是否是自己的锁 再进行解锁
           //解锁
            //lua 脚本
            String lua = "if redis.call('get',KEYS[1]) == ARGV[1] " +
                         "then" +
                         " return redis.call('del',KEYS[1]) " +
                         "else" +
                         " return 0 " +
                         "end";
            strRedisTemplate.execute(new DefaultRedisScript<>(lua, Boolean.class), Arrays.asList("lock"),uuid);
//            if (uuid.equals(strRedisTemplate.opsForValue().get("lock"))){
//                strRedisTemplate.delete("lock");
//            }
        }
    }

 修改库存stock为1000,重启服务开始压测:

压测后库存结果

分布式锁系列之Redis分布式锁_第128张图片

 压测日志:

分布式锁系列之Redis分布式锁_第129张图片

锁可重入性问题 

此时上面的锁操作解决了锁误删的情况,但是还是有一些问题

比如线程在调用A方法时会拿到一个lock锁,后面执行业务方法,业务方法中有个B方法,B方法也需要拿到一个lock锁,此时A方法的lock锁还没有释放,所以此时B方法会一直等待A方法释放锁,这个时候就又产生了死锁的问题,此时就需要使用到锁的可重入性来进行解决

使用exists 指令来判断锁是否存在  存在返回1 不存在返回0

分布式锁系列之Redis分布式锁_第130张图片

如果不存在,则使用hset来进行设置锁 并设置获取锁的次数

 hset 锁名 锁的值 获取锁的次数分布式锁系列之Redis分布式锁_第131张图片

 分布式锁系列之Redis分布式锁_第132张图片

 

 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 1 lock 123 30 

 分布式锁系列之Redis分布式锁_第133张图片

if redis.call('hexists',KEYS[1],ARGV[1]) == 0 then return nil elseif redis.call('hincrby',KEYS[1],ARGV[1],-1)== 0 then return redis.call('del',KEYS[1]) else return 0 end 1 lock 123

分布式锁系列之Redis分布式锁_第134张图片

代码实现:

这里需要书写lock锁工具类并继承lock

package com.example.demo.utils;

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.Arrays;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

public class DistributeRedisLock implements Lock {

    public String lockName;
    private StringRedisTemplate stringRedisTemplate;

    private String uuid;

    private long expire = 30;
    public DistributeRedisLock(){}

    public DistributeRedisLock(String lockName, StringRedisTemplate stringRedisTemplate,String uuid){
        this.lockName = lockName;
        this.stringRedisTemplate = stringRedisTemplate;
        //为线程拼唯一id
        this.uuid = uuid+":"+Thread.currentThread().getId();
    }


    @Override
    public void lock() {
        this.tryLock();
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public boolean tryLock() {
        try {
            return this.tryLock(-1L,TimeUnit.SECONDS);
        } catch (InterruptedException e){
            e.printStackTrace();
        }
        return false;
    }

    /**
     * 加锁
     * @param time the maximum time to wait for the lock
     * @param unit the time unit of the {@code time} argument
     * @return
     * @throws InterruptedException
     */
    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        if(time != -1){
            this.expire = unit.toSeconds(time);
        }
        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";
        while (!this.stringRedisTemplate.execute(new DefaultRedisScript<>(
                script, Boolean.class), Arrays.asList(lockName),uuid,String.valueOf(expire))){
            Thread.sleep(50);
        }
        return true;
    }


    /**
     *放锁
     */
    @Override
    public void unlock() {
       String script = "if redis.call('hexists',KEYS[1],ARGV[1]) == 0 " +
                       "then " +
                           "return nil " +
                       "elseif redis.call('hincrby',KEYS[1],ARGV[1],-1)== 0 " +
                       "then " +
                           "return redis.call('del',KEYS[1]) " +
                       "else " +
                          "return 0 " +
                       "end";
       Long flag = stringRedisTemplate
               .execute(new DefaultRedisScript<>(script,Long.class),Arrays.asList(lockName),uuid);
       if(flag==null){
           throw new IllegalMonitorStateException("这把锁不属于该线程");
       }
    }

    @Override
    public Condition newCondition() {
        return null;
    }
}

书写一个工厂,来获取锁工具

package com.example.demo.factory;

import com.example.demo.utils.DistributeRedisLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.UUID;

@Component
public class DistributedLockClient {

    @Autowired
    StringRedisTemplate stringRedisTemplate;
    
    private String uuid;
    
    public DistributedLockClient(){
        this.uuid = UUID.randomUUID().toString();
    }
    public DistributeRedisLock getRedisLock(String lockName){
        return new DistributeRedisLock(lockName,stringRedisTemplate,uuid);
    }
}

服务层调用

分布式锁系列之Redis分布式锁_第135张图片

 启动服务,将库存调整为1000后开始压测

 压测日志:

分布式锁系列之Redis分布式锁_第136张图片

库存数量:

 分布式锁系列之Redis分布式锁_第137张图片

 测试可重入锁情况:

书写一个方法,使得获取的锁名字和服务层中获取锁名一致,服务层业务逻辑再调用这个方法

分布式锁系列之Redis分布式锁_第138张图片

 重启测试

打断点

分布式锁系列之Redis分布式锁_第139张图片

查看此时锁的获取状态

 分布式锁系列之Redis分布式锁_第140张图片

可以看到其被获取的次数值是1,

放断点

 分布式锁系列之Redis分布式锁_第141张图片

再次查看锁的状态 此时锁的获取次数变为了2,说明这个锁可以被重入  注意操作一定要快,在默认30秒过期时间内操作完毕

 分布式锁系列之Redis分布式锁_第142张图片

 重启服务压测

库存:

分布式锁系列之Redis分布式锁_第143张图片

 压测日志:

分布式锁系列之Redis分布式锁_第144张图片

锁自动续期 

可以看到锁的可重入性得到了解决,可是也暴露出一个问题,那就是锁的过期时间,如果方法中的业务执行时间超过了锁的过期时间的话此时锁就会自动释放,此时高并发情况下锁就会被其他线程所拿走,所以这里需要添加一个锁的自动续期来规避这个问题

分布式锁系列之Redis分布式锁_第145张图片

if redis.call('hexists',KEYS[1],ARGV[1]) == 1 then return redis.call('expire',KEYS[1],ARGV[2]) else return 0 end

测试续期

分布式锁系列之Redis分布式锁_第146张图片

 分布式锁系列之Redis分布式锁_第147张图片

可以看到此时lock这个键值是没有过期时间的(也可以直接设置一个默认初始就有过期时间来进行测试)

执行续期指令 

分布式锁系列之Redis分布式锁_第148张图片

 分布式锁系列之Redis分布式锁_第149张图片

电脑有些卡顿,切换到redis图形化查看时就剩14秒就过期了o(╥﹏╥)o

赶紧再次执行一次续命

 分布式锁系列之Redis分布式锁_第150张图片

再次查看过期时间

 分布式锁系列之Redis分布式锁_第151张图片

指令生效

 代码实现

lock工具类添加自动续期方法 这里设置锁过期时间为过去三分之一时自动续期,也就是10秒

/**
     *  自动续期
     * @return
     */
    private void renewExpire(){
        String script = "if redis.call('hexists',KEYS[1],ARGV[1]) == 1 " +
                        "then " +
                          "return redis.call('expire',KEYS[1],ARGV[2]) " +
                        "else " +
                          "return 0 " +
                        "end";
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                if (stringRedisTemplate.execute(new DefaultRedisScript<>(script,Boolean.class), Arrays.asList(lockName),uuid,String.valueOf(expire))) {
                    renewExpire();
                }
            }
        },this.expire * 1000 / 3);
    }

加锁成功后调用自动续期方法

分布式锁系列之Redis分布式锁_第152张图片

服务类中模拟业务长时间执行

 分布式锁系列之Redis分布式锁_第153张图片

重启服务测试自动续期

 发起请求

 分布式锁系列之Redis分布式锁_第154张图片

 查看redis锁过期时间

 可以看到过期时间在到了20秒时自动续期了

解除掉模拟业务长时间执行的线程睡眠,再重启服务开始高并发压测

分布式锁系列之Redis分布式锁_第155张图片

重启服务,设置库存1000,开始压测

分布式锁系列之Redis分布式锁_第156张图片

开始压测

分布式锁系列之Redis分布式锁_第157张图片

 可以看到库存变为0

查看压测日志

分布式锁系列之Redis分布式锁_第158张图片

 小总结

redis锁特征

分布式锁系列之Redis分布式锁_第159张图片

 分布式锁系列之Redis分布式锁_第160张图片

 分布式锁系列之Redis分布式锁_第161张图片

 Redisson分布式锁

介绍

Redisson是一款基于Redis实现的分布式框架,其中包含了分布式锁的实现。分布式锁是一种保证分布式系统中多个节点并发访问共享资源时,只有一个节点能够访问共享资源的机制。Redisson分布式锁基于Redis实现,利用Redis提供的一些指令,实现了分布式锁的功能。

具体来说,Redisson分布式锁的实现如下:

  1. 客户端通过Redisson框架向Redis服务器发送锁定请求,请求中包含锁的名称和锁的超时时间。

  2. 如果Redis服务器没有锁定该名称的锁,则客户端可以获得锁,并返回成功。

  3. 如果Redis服务器已经锁定了该名称的锁,则客户端需要等待,直到超时时间到或者其他客户端释放了该锁。

  4. 如果超时时间到了,客户端仍然没有获得锁,则释放该锁,并返回超时错误。

  5. 客户端在获得锁之后,在指定的超时时间内没有释放锁,则锁会自动释放。

  6. 如果其他客户端在锁释放之前也请求了该锁,则只有一个客户端能够获得锁,其他客户端需要等待。

Redisson分布式锁支持多种锁策略,包括排它锁、读写锁、共享锁等。其中,排它锁是一种独占锁,只有一个客户端能够获得该锁,其他客户端需要等待。读写锁是一种可重入的多路锁,允许多个客户端同时读取共享资源,但只有一个客户端能够写入共享资源。共享锁是一种多路锁,允许多个客户端同时读取共享资源,但是只有一个客户端能够写入共享资源。

总之,Redisson分布式锁基于Redis实现,具有可靠性、性能高、易于使用等特点,是一种常用的分布式锁实现方式。

简单使用可重入锁

引入所需依赖

 
      org.redisson
      redisson
      3.23.3
 

书写配置器

分布式锁系列之Redis分布式锁_第162张图片

分布式锁系列之Redis分布式锁_第163张图片

 使用

分布式锁系列之Redis分布式锁_第164张图片

更改库存为1000,启动服务开始压测

分布式锁系列之Redis分布式锁_第165张图片

开始压测

 分布式锁系列之Redis分布式锁_第166张图片

查看压测日志可以看到其性能要比原redis要好一些

分布式锁系列之Redis分布式锁_第167张图片

公平锁

分布式锁系列之Redis分布式锁_第168张图片

书写公平锁

服务层

分布式锁系列之Redis分布式锁_第169张图片

 控制层:

分布式锁系列之Redis分布式锁_第170张图片

启动两个服务,测试公平锁

 

可以看到获取锁的顺序是按照请求的顺序1,2,3,4,5一一获取的 

如果换成普通的锁是否还会按照请求顺序获取锁呢,可以尝试下

分布式锁系列之Redis分布式锁_第171张图片

 换成普通的锁,再次重启服务测试

可以看到这次获取锁的顺序是1,2,4,3,5 并没有按照请求的顺序正常获取锁

联锁(只做了解)

 

 红锁(只做了解)

 读写锁

分布式锁系列之Redis分布式锁_第172张图片

书写读写锁

服务层

分布式锁系列之Redis分布式锁_第173张图片 控制层

分布式锁系列之Redis分布式锁_第174张图片

重启服务测试

分布式锁系列之Redis分布式锁_第175张图片

可以看到读锁是可以并发进行的,而写锁不可以并发进行 

RSemaphore信号量

RSemaphore是Redisson提供的一种分布式信号量(Semaphore)实现。

信号量是一种用于控制多个进程或线程并发访问临界资源的机制。它可以用来限制对某个资源的并发访问数量,比如限制同时访问某个数据库连接池的线程数。

RSemaphore提供了以下几个主要方法:

  1. acquire(): 获取一个信号量,如果当前信号量数量为0,则会阻塞等待。
  2. tryAcquire(): 尝试获取一个信号量,如果当前信号量数量为0,则立即返回false,否则返回true。
  3. tryAcquire(long timeout, TimeUnit unit): 尝试获取一个信号量,在指定的时间内等待,如果在指定时间内获取到信号量,则返回true,否则返回false。
  4. release(): 释放一个信号量,将信号量数量加1。
  5. release(int permits): 释放指定数量的信号量,将信号量数量增加指定数量。

RSemaphore的作用是在分布式环境下实现并发访问控制。通过RSemaphore,可以限制对某个共享资源的并发访问数量,保证资源的正确使用。例如,在某个分布式系统中,有多个服务需要同时访问某个数据库连接池,为了避免连接池资源被过度使用,可以使用RSemaphore限制同时访问连接池的服务数量,从而保证连接池的稳定性和性能。

需要注意的是,RSemaphore是基于Redis的分布式锁实现的,因此要求所有使用RSemaphore的分布式节点都能够访问同一个Redis实例。

分布式锁系列之Redis分布式锁_第176张图片

juc实现信号量

分布式锁系列之Redis分布式锁_第177张图片

观察打印日志,始终都是停车位满了之后占位车开走之后 其他车才可以继续抢车位,看下如果不使用信号量会出现什么情况

分布式锁系列之Redis分布式锁_第178张图片

可以一上来就抢了6个车位,可是只有三个车位哪来的6个车位可以抢啊

redisson实现信号量

服务层:

分布式锁系列之Redis分布式锁_第179张图片

控制层

分布式锁系列之Redis分布式锁_第180张图片

重启服务开始压测

信号量初始3 

分布式锁系列之Redis分布式锁_第181张图片

 发起一个请求

分布式锁系列之Redis分布式锁_第182张图片

分布式锁系列之Redis分布式锁_第183张图片

信号量减一

开始高并发压测

可以看到sempore初始资源量为3,资源量达到3时,每次等到其他资源释放时才会有其他资源来占用,并且资源量永远不会超过3,从而实现了限流

RCountDownLatch倒计数器 

分布式锁系列之Redis分布式锁_第184张图片

试用场景,一个线程等待其他线程全部执行结束后再执行

juc实现CountDownLatch:

分布式锁系列之Redis分布式锁_第185张图片

 可以看到班长线程是在其他线程都执行结束后才执行锁门,再看下如果不使用CountDownLatch情况

分布式锁系列之Redis分布式锁_第186张图片 可以看到学生们还没走完甚至都没走时班长就锁门自己走了,这着实有些离谱啊Σ(⊙▽⊙"a

redisson实现

服务层

分布式锁系列之Redis分布式锁_第187张图片

控制层:

分布式锁系列之Redis分布式锁_第188张图片

启动服务测试: 

 可以看到班长接口一直在加载中,只有当6个学生接口都执行完毕后班长接口才会有响应

Redisson小结

Redisson是一个基于Redis的分布式Java对象和服务框架。它提供了一系列的分布式对象和服务,包括分布式集合、分布式锁、分布式队列等,方便开发者在分布式环境中进行数据共享和并发控制。

Redisson的主要特点包括:

  1. 简单易用:Redisson提供了简单易用的API,使得开发者可以轻松地在Java应用中使用分布式对象和服务。

  2. 高性能:Redisson采用了高效的序列化和压缩技术,以及异步IO和线程池等技术,提供了高性能的分布式对象和服务。

  3. 高可用性:Redisson通过Redis的主从复制和哨兵机制,以及集群模式,实现了高可用性和容错性。

  4. 分布式锁:Redisson提供了基于Redis的分布式锁实现,支持公平锁和非公平锁,并提供了可重入锁和读写锁等常用的锁类型。

  5. 分布式集合:Redisson提供了分布式集合的实现,包括Set、List、Queue、Deque等,支持分布式并发操作。

  6. 分布式队列:Redisson提供了分布式队列的实现,支持异步操作和延迟任务,方便实现消息队列和任务调度等功能。

  7. 分布式对象映射:Redisson提供了分布式对象映射的功能,可以将Java对象存储到Redis中,并提供了对象锁和对象监听等特性。

你可能感兴趣的:(java,分布式锁,分布式,分布式锁,redis,redisson,悲观锁,乐观锁)