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

news2025/1/16 11:15:23

目录

介绍

模拟订单超卖场景

代码版

不加锁情况

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件库存

不加锁情况

 服务层

控制层

 

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

测试:

 

 

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

synchronized加锁

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

 重新测试:

 

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

lock加锁

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

 测试:

 

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

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

传统不加锁压测结果:

 synchronized加锁压测结果:

 lock加锁压测情况:

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

整合Mysql版

创建商品表

插入测试数据

 对应实体类

@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;


}

不加锁版 

服务层

 启动测试:

最终idea日志

 数据库数据:

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

压测日志:

 synchronized加锁

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

重新压测:

 

 结果符合预期

压测日志

 lock加锁版

记得库存调回1000

测试:

压测日志:

 

 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版

重新测试:

 

synchronized版

 

 

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

事务

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

添加默认事务,可重复读

 测试

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

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

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

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

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

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

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

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

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

 集群搭建

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

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

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

 

 启动两个实例

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

配置nginx:

 

 

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

 

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

说明启动成功

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

 

 由nginx代理到本地的集群服务

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

 两台服务器最后的日志都是472

查看数据库:

压测结果

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

书写sql解决集群超卖

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

持久层

 服务层 

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

 

 两个集群服务都显示最后的库存为0和数据库的库存一致都达到了理想的结果0

 压测结果

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

1. 锁的范围问题 

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

初始化数据

 开启事务

 更改

查看数据有无变化

没有变化

再更新华为P30

 

 还是没变化

提交

 查看数据变化

这个时候数据才有变化

mysql悲观锁中使用行级锁:

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

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

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

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

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

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

使用悲观锁select ...for update

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

 不加悲观锁情况

开启事务

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

 

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

使用for update加悲观锁

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

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

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

左边事务进行提交

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

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

整合项目使用悲观锁

修改mapper层

服务层

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

iphone13数量改为1000

启动两个服务,开始压测

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

 压测日志

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

再次压测

 结果为0并没有发生超卖情况 

悲观锁的问题

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

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

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

 左侧再去获取id为2的锁

发现光标一直在等待

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

 

 报错死锁

(3) 库存操作要一致

 这样也会出问题

使用乐观锁version

实现乐观锁

在数据库中添加version字段

服务层

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

测试结果

 

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

压测日志

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

乐观锁存在的问题

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

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

Mysql锁总结

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

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

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

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

优先选择:mysql悲观锁

不推荐jvm本地锁。

Redis

使用redis模拟超卖场景

启动redis,设置库存1000

添加redsi所需依赖

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

编写服务层

启动服务开始压测

查询redis库存

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

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

 redis乐观锁实现

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

multi: 开启事务

exec: 执行事务

服务层

将redis库存重新设置为1000

开始压测

 查看redis库存

 库存变为0符合预期结果

查看压测日志

 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

 

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

 

结合项目

服务层

将redis中库存调整为1000

启动两个服务进行压测

 

压测日志

 刚才示例中使用的是递归调用,一定程度上性能会有问题,可以调整为循环调用

 

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

压测日志

分布式锁特征2

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

分布式特征 3

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

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

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

分布式锁特征4

防误删

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

 代码改造

上述改动虽然可以使用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文档

 

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

 

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

lua脚本流程控制

 分支控制格式:

if 条件

then 

  代码块

elseif 条件

then

  代码块

else

  代码块

end

演示:

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

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

举例:

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

 

测试:

千万注意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,重启服务开始压测:

压测后库存结果

 压测日志:

锁可重入性问题 

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

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

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

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

 hset 锁名 锁的值 获取锁的次数

 

 

 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 

 

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

代码实现:

这里需要书写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);
    }
}

服务层调用

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

 压测日志:

库存数量:

 

 测试可重入锁情况:

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

 重启测试

打断点

查看此时锁的获取状态

 

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

放断点

 

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

 

 重启服务压测

库存:

 压测日志:

锁自动续期 

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

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

测试续期

 

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

执行续期指令 

 

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

赶紧再次执行一次续命

 

再次查看过期时间

 

指令生效

 代码实现

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锁过期时间

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

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

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

开始压测

 可以看到库存变为0

查看压测日志

 小总结

redis锁特征

 

 

 Redisson分布式锁

介绍

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

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

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

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

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

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

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

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

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

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

简单使用可重入锁

引入所需依赖

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

书写配置器

 使用

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

开始压测

 

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

公平锁

书写公平锁

服务层

 控制层:

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

 

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

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

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

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

联锁(只做了解)

 

 红锁(只做了解)

 读写锁

书写读写锁

服务层

 控制层

重启服务测试

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

RSemaphore信号量

RCountDownLatch倒计数器

Redisson小结

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

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

相关文章

备份或同步数据?跨国大文件传输的不同需求与解决方案

信息化时代的到来&#xff0c;使得许多个人、组织、企业在日常生活中都需要对数据进行备份或同步。但不同的应用场景和需求&#xff0c;也需要不同的备份和同步方式。而在跨国大文件传输方面&#xff0c;更是需要根据不同需求选择合适的传输方案。下面将分别介绍备份与同步数据…

第一章MyBatis概述

框架 Java常用框架 SSM三大框架&#xff1a;SpringSpringMVCMyBatisSpringBootSpringCloud 框架简单来说就是提前封装好的通用代码&#xff0c;提高我们的开发的效率&#xff08;站在巨人的肩膀上&#xff09; 框架一般以jar包的形式存在 三层架构 代码封装主要为了降低代…

MQTT 常用客户端库介绍 (全面涵盖c,c++,java,c#,python)

MQTT&#xff08;Message Queuing Telemetry Transport&#xff09;是一种轻量级的通信协议&#xff0c;被广泛应用于物联网和分布式系统中。它以其简单、可靠和高效的特性而备受推崇&#xff0c;成为连接设备和应用程序的首选协议。MQTT的重要性不言而喻&#xff0c;它为实时通…

K8S应用笔记 —— 签发自签名证书用于Ingress的https配置

一、需求描述 在本地签发自命名证书&#xff0c;用于K8S集群的Ingress的https配置。 前提条件&#xff1a; 完成K8S集群搭建。完成证书制作机器的openssl服务安装。 二、自签名证书制作 2.1 脚本及配置文件准备 2.1.1 CA.sh脚本准备 注意事项&#xff1a; openssl服务默认CA…

【vim 学习系列文章 5 - cscope 过滤掉某些目录】

文章目录 cscope 过滤目录介绍 cscope 过滤目录介绍 第一步创建自己的cscope脚本~/.local/bin/cscope.sh&#xff0c;如下&#xff1a; function my_cscope() {CODE_PATHpwdecho "$CODE_PATH"echo "start cscope...."if [ ! -f "$CODE_PATH/cscope.…

qt实现9×9数独游戏

qt实现的数独小游戏&#xff0c;资源有可下载直接跑的exe&#xff08;enigma已经打包好&#xff09;&#xff0c;源码可私信 部分代码 #include "widget.h" #include "ui_widget.h" #include"form.h" #include<QDebug> #include<QPush…

数据结构——二叉搜索树(附带C++实现版本)

文章目录 二叉搜索树概念 二叉树的实际应用二叉树模拟实现存储结构二叉搜索树构成二叉搜索树的查找插入操作中序遍历二叉树的删除循环(利用左子树最右节点&#xff09;递归(利用右子树根节点) 二叉树拷贝二叉树资源的销毁 二叉树实现完整代码总结 二叉搜索树 概念 二叉搜索树…

LVS-DR+keepalived实现高可用负载群集

VRRP 通信原理&#xff1a; VRRP就是虚拟路由冗余协议&#xff0c;它的出现就是为了解决静态路由的单点故障。 VRRP是通过一种竞选的一种协议机制&#xff0c;来将路由交给某台VRRP路由。 VRRP用IP多播的方式&#xff08;多播地址224.0.0.18&#xff09;来实现高可用的通信&…

opencv运动目标检测-背景建模

背景建模 帧差法 由于场景中的目标在运动&#xff0c;目标的影像在不同图像帧中的位置不同。该类算法对时间上连续的两帧图像进行差分运算&#xff0c;不同帧对应的像素点相减&#xff0c;判断灰度差的绝对值&#xff0c;当绝对值超过一定阈值时&#xff0c;即可判断为运动目…

Java虚拟机(JVM):垃圾收集算法

目录 一、分代收集理论 二、标记-清除算法 三、标记-复制算法 四、标记-整理算法 一、分代收集理论 分代收集理论建立在两个分代假说之上&#xff1a; 1、弱分代假说&#xff1a;绝大多数对象都是朝生夕灭的。 2、强分代假说&#xff1a;熬过越多次垃圾收集过程的对象就…

Python中数据结构列表详解

列表是最常用的 Python 数据类型&#xff0c;它用一个方括号内的逗号分隔值出现&#xff0c;列表的数据项不需要具有相同的类型。 列表中的每个值都有对应的位置值&#xff0c;称之为索引&#xff0c;第一个索引是 0&#xff0c;第二个索引是 1&#xff0c;依此类推。列表都可…

C语言之指针进阶篇(1)

目录​​​​​​​ 引言 字符指针 指针数组 数组指针 数组指针的定义 &数组名vs数组名 数组指针的使用 一维数组使用 二维数组使用 一维数组传参 二维数组传参 总结 数组参数 一维数组传参 二维数组传参 指针参数 一级指针传参 二级指针传参 引言 今…

Jmeter对websocket进行测试

JMeterWebSocketSampler-1.0.2-SNAPSHOT.jar下载 公司使用websocket比较奇怪&#xff0c;需要带认证信息进行长连接&#xff0c;通过websocket插件是请求失败&#xff0c;如下图&#xff0c;后面通过代码实现随再打包jar包完成websocket测试 本地实现代码如下&#xff1a; pa…

总结,由于顺丰的问题,产生了电脑近期一个月死机问题集锦

由于我搬家&#xff0c;我妈搞顺丰发回家&#xff0c;但是没有检查有没有坏&#xff0c;并且我自己由于不可抗力因素&#xff0c;超过了索赔时间&#xff0c;反馈给顺丰客服&#xff0c;说超过了造成了无法索赔的情况&#xff0c;现在总结发生了损坏配件有几件&#xff0c;显卡…

Java 项目日志实例基础:Log4j

点击下方关注我&#xff0c;然后右上角点击...“设为星标”&#xff0c;就能第一时间收到更新推送啦~~~ 介绍几个日志使用方面的基础知识。 1 Log4j 1、Log4j 介绍 Log4j&#xff08;log for java&#xff09;是 Apache 的一个开源项目&#xff0c;通过使用 Log4j&#xff0c;我…

奥威BI数据可视化工具:个性化定制,打造独特大屏

每个人都有自己独特的审美&#xff0c;因此即使是做可视化大屏&#xff0c;也有很多人希望做出不一样的报表&#xff0c;用以缓解审美疲劳的同时提高报表浏览效率。因此这也催生出了数据可视化工具的个性化可视化大屏制作需求。 奥威BI数据可视化工具&#xff1a;个性化定制&a…

nginx代理webSocket链接响应403

一、场景 使用nginx代理webSocket链接&#xff0c;nginx响应403 1、nginx访问日志响应403 [18/Aug/2023:09:56:36 0800] "GET /FS_WEB_ASS/webim_api/socket/message HTTP/1.1" 403 5 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit…

opencv-dnn

# utils_words.txt 标签文件 import osimage_types (".jpg", ".jpeg", ".png", ".bmp", ".tif", ".tiff")def list_images(basePath, containsNone):# return the set of files that are validreturn list_file…

机器学习|决策树:数学原理及代码解析

机器学习&#xff5c;决策树&#xff1a;数学原理及代码解析 决策树是一种常用的监督学习算法&#xff0c;适用于解决分类和回归问题。在本文中&#xff0c;我们将深入探讨决策树的数学原理&#xff0c;并提供 Python 示例代码帮助读者更好地理解和实现该算法。 决策树数学原…

大语言模型-RLHF(七)-PPO实践(Proximal Policy Optimization)原理实现代码逐行注释

从open AI 的论文可以看到&#xff0c;大语言模型的优化&#xff0c;分下面三个步骤&#xff0c;SFT&#xff0c;RM&#xff0c;PPO&#xff0c;我们跟随大神的步伐&#xff0c;来学习一下这三个步骤和代码实现&#xff0c;本章介绍PPO实践。 生活中&#xff0c;我们经常会遇到…