从库存超卖问题分析锁和分布式锁的应用(二)

news2024/11/22 17:05:19

本文从一个经典的库存超卖问题分析说明常见锁的应用,假设库存资源存储在Redis里面。

假设我们的减库存代码如下:

@Autowired
StringRedisTemplate redisTemplate;

public void deduct(){
    String stock = redisTemplate.opsForValue().get("stock");
    if(StringUtils.hasLength(stock)){
        Integer st = Integer.valueOf(stock);
        if(st>0){
            redisTemplate.opsForValue().set("stock",String.valueOf(--st));
        }
    }
}

此时方法操作是先读后写,非原子性操作,是存在并发问题的。如何解决该问题,有三种方案:

  • JVM本地锁
  • Redis乐观锁
  • Redis实现分布式锁

JVM本地锁的实现与优缺点在从库存超卖问题分析锁和分布式锁的应用(一)已经分析过了,这里不再赘述。

【1】Redis乐观锁

也就是watchmultiexec组合指令的使用。

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

multi用来开启事务,exec用来提交/执行事务。

watch stock
multi
set stock 5000
exec

代码修改如下:

public void deduct(){
    this.redisTemplate.execute(new SessionCallback() {
        @Override
        public Object execute(RedisOperations operations) throws DataAccessException {
            operations.watch("stock");
            // 1. 查询库存信息
            Object stock = operations.opsForValue().get("stock");
            // 2. 判断库存是否充足
            int st = 0;
            if (stock != null && (st = Integer.parseInt(stock.toString())) > 0) {
                // 3. 扣减库存
                operations.multi();//开启事务
                operations.opsForValue().set("stock", String.valueOf(--st));
                List exec = operations.exec();//执行事务
                if (exec == null || exec.size() == 0) {
                    try {
                    // 这里睡眠一下,降低竞争,提高乐观锁的吞吐量
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //再次递归
                    deduct();
                }
                return exec;
            }
            return null;
        }
    });
}

这种方式确实可以解决并发问题,但也可能在高并发的情况下由于不断重试(CAS思想)出现性能问题、连接被耗尽的情况。

【2】Redis分布式锁

① 基于setnx思想简单实现

借助于redis中的命令setnx(key, value),key不存在就新增,存在就什么都不做。同时有多个客户端发送setnx命令,只有一个客户端可以成功,返回1(true);其他的客户端返回0(false)。

// 递归思想
public void deduct(){
     //获取锁
     Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "1111");
     //如果获取不到则递归重试
     if(!lock){
         deduct();
     }else{
         try{
             String stock = redisTemplate.opsForValue().get("stock");
             if(StringUtils.hasLength(stock)){
                 Integer st = Integer.valueOf(stock);
                 if(st>0){
                     redisTemplate.opsForValue().set("stock",String.valueOf(--st));
                 }
             }
         }finally {
             //释放锁
             redisTemplate.delete("lock");
         }
     }
 }

或者使用while思想:

public void deduct(){
//当setnx刚刚获取到锁,当前服务器宕机,导致del释放锁无法执行,进而导致锁无法锁无法释放(死锁)
     while (Boolean.FALSE.equals(this.redisTemplate.opsForValue().setIfAbsent("lock", "xxx"))){
         try {
             Thread.sleep(100);
         }catch (Exception e){
             e.printStackTrace();
         }
     }
     try{
         String stock = redisTemplate.opsForValue().get("stock");
         if(StringUtils.hasLength(stock)){
             Integer st = Integer.valueOf(stock);
             if(st>0){
                 redisTemplate.opsForValue().set("stock",String.valueOf(--st));
             }
         }
     }finally {
         //释放锁
         redisTemplate.delete("lock");
     }
 }

这种方式存在问题:当setnx刚刚获取到锁,当前服务器宕机,导致del释放锁无法执行,进而导致锁无法锁无法释放(死锁)

解决方案:给锁设置过期时间,自动释放锁。

设置过期时间两种方式:

  1. 通过expire设置过期时间(缺乏原子性:如果在setnx和expire之间出现异常,锁也无法释放)
  2. 使用set指令设置过期时间:set key value ex 3 nx(既达到setnx的效果,又设置了过期时间)

② 防死锁优化

修改while中获取锁的逻辑如下所示:

while (Boolean.FALSE.equals(this.redisTemplate.opsForValue().setIfAbsent("lock", "xxx",3, TimeUnit.SECONDS)){
    try {
        Thread.sleep(100);
    }catch (Exception e){
        e.printStackTrace();
    }
}

这种方式解决了死锁问题但是可能会释放其他服务器的锁。

场景:如果业务逻辑的执行时间是7s。执行流程如下

  1. index1业务逻辑没执行完,3秒后锁被自动释放。
  2. index2获取到锁,执行业务逻辑,3秒后锁被自动释放。
  3. index3获取到锁,执行业务逻辑
  4. index1业务逻辑执行完成,开始调用del释放锁,这时释放的是index3的锁,导致index3的业务只
    执行1s就被别人释放。最终等于没锁的情况。

解决:setnx获取锁时,设置一个指定的唯一值(例如:uuid);释放前获取这个值,判断是否自己的锁

③ 防误删优化

如下这里设置锁的密钥为UUID,加锁者持有。

public void deduct(){
    String uuid = UUID.randomUUID().toString();
    while (Boolean.FALSE.equals(this.redisTemplate.opsForValue().setIfAbsent("lock", uuid,3, TimeUnit.SECONDS)){
        try {
            Thread.sleep(100);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    try{
        String stock = redisTemplate.opsForValue().get("stock");
        if(StringUtils.hasLength(stock)){
            Integer st = Integer.valueOf(stock);
            if(st>0){
                redisTemplate.opsForValue().set("stock",String.valueOf(--st));
            }
        }
    }finally {
        //释放锁
        if(uuid.equals(redisTemplate.opsForValue().get("lock"))){
            redisTemplate.delete("lock");
        }
    }
}

这种方式仍旧存在问题:删除操作缺乏原子性。

场景:

  1. index1执行删除时,查询到的lock值确实和uuid相等
  2. index1执行删除前,lock刚好过期时间已到,被redis自动释放
  3. index2获取了lock
  4. index1执行删除,此时会把index2的lock删除

解决方案:没有一个命令可以同时做到判断 + 删除,所有只能通过其他方式实现(LUA脚本

④ lua脚本保证删除原子性

redis采用单线程架构,可以保证单个命令的原子性,但是无法保证一组命令在高并发场景下的原子性。

如下AB两个进程示例:

在这里插入图片描述
在串行场景下:A和B的值肯定都是3。在并发场景下:A和B的值可能在0-6之间。

极限情况下1:则A的结果是0,B的结果是3

在这里插入图片描述

极限情况下2:则A和B的结果都是6

在这里插入图片描述

如果redis客户端通过lua脚本把3个命令一次性发送给redis服务器,那么这三个指令就不会被其他客户端指令打断。Redis 也保证脚本会以原子性(atomic)的方式执行: 当某个脚本正在运行的时候,不会有其他脚本或 Redis 命令被执行。 这和使用 MULTI/ EXEC 包围的事务很类似。

但是MULTI/ EXEC方法来使用事务功能,将一组命令打包执行,无法进行业务逻辑的操作。这期间有某一条命令执行报错(例如给字符串自增),其他的命令还是会执行,并不会回滚。

优化代码如下所示:

public void deduct(){
     String uuid = UUID.randomUUID().toString();
     while (Boolean.FALSE.equals(this.redisTemplate.opsForValue().setIfAbsent("lock", uuid,3, TimeUnit.SECONDS))){
         try {
             Thread.sleep(100);
         }catch (Exception e){
             e.printStackTrace();
         }
     }
     try{
         String stock = redisTemplate.opsForValue().get("stock");
         if(StringUtils.hasLength(stock)){
             int st = Integer.parseInt(stock);
             if(st>0){
                 redisTemplate.opsForValue().set("stock",String.valueOf(--st));
             }
         }
     }finally {
         // 先判断是否自己的锁,再解锁
         String script = "if redis.call('get', KEYS[1]) == ARGV[1] " +
                 "then " +
                 "   return redis.call('del', KEYS[1]) " +
                 "else " +
                 "   return 0 " +
                 "end";
         this.redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Collections.singletonList("lock"), uuid);
//            //释放锁
//            if(uuid.equals(redisTemplate.opsForValue().get("lock"))){
//                redisTemplate.delete("lock");
//            }
     }
 }

到这里似乎完美解决了我们考虑到的几点问题,那么结束了吗?

并没有,目前这种方式不支持可重入性、并且集群环境下也存在失效情况。更甚者如果由于异常情况,获取锁后服务逻辑未执行完毕,锁就自动释放了呢

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

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

相关文章

docker desktop历史版本安装

1.安装choco Windows安装 choco包管理工具-CSDN博客 2.通过choco安装 下面例子为安装旧版2.3.0.2,其它版本类似 Chocolatey Software | Docker Desktop 2.3.0.2 https://download.docker.com/win/stable/45183/Docker%20Desktop%20Installer.exe choco install docker-des…

linux高级编程(网络)(www,http,URL)

数据的封包和拆包 封包&#xff1a; 应用层数据&#xff08;例如HTTP请求&#xff09;被传递给传输层。传输层&#xff08;TCP&#xff09;在数据前添加TCP头部&#xff08;包含端口号、序列号等&#xff09;。网络层&#xff08;IP&#xff09;在TCP段前添加IP头部&#xff…

【从0到1进阶Redis】主从复制 — 主从机宕机测试

上一篇&#xff1a;【从0到1进阶Redis】主从复制 测试&#xff1a;主机断开连接&#xff0c;从机依旧连接到主机的&#xff0c;但是没有写操作&#xff0c;这个时候&#xff0c;主机如果回来了&#xff0c;从机依旧可以直接获取到主机写的信息。 如果是使用命令行&#xff0c;来…

底软驱动 | 大厂面试爱考的C++内存相关

文章目录 C内存相关C内存分区C对象的成员函数存放在内存哪里 堆和栈的区别堆和栈的访问效率“野指针”有了malloc/free为什么还要new/deletealloca内存崩溃C内存泄漏的几种情况内存对齐柔性数组参考推荐阅读 C内存相关 本篇介绍了 C 内存相关的知识。 C内存分区 在C中&#…

【python学习】numpy第三方库的定义、功能、使用场景和使用以及遇到的一些问题

引言 python学习学习到第三方库知识&#xff0c;首先学习的就是机器学习以及对应的numpy第三方库 文章目录 引言一、numpy第三方库的定义二、numpy第三方库的功能2.1数组操作2.2 线性代数计算2.3 随机数生成2.4 文件读写 三、numpy第三方库的使用场景3.1需要进行数值计算3.2 需…

PyCharm软件初始化配置

安装完pycharm后&#xff0c;需要对其进行个性化设置&#xff0c;分别设置方法如下 目录 一、修改主题二、修改默认字体和大小三、设置拖动滚轮改变字体大小四、常见快捷键 一、修改主题 1、界面右上角点击红框的内容 2、选择Theme选项 3、选择对应的主题 第一二个是白色主题…

通俗易懂多图透彻讲解二叉树的遍历--前序, 中序和后序

二叉树的遍历是一个数据结构中经常会遇到的知识点, 具体又分为前序, 中序和后序三种. 什么是树? 先来理解一下什么是树, 从一个我们相对熟悉的家谱树(Family Tree)说起吧. 家族的根是爷爷, 然后生了两个娃, 大伯和你爸爸. 继续往下, 有堂哥堂姐, 还有你以及你妹, 等等. 一个…

工业智能网关的边缘计算能力赋能工业4.0

边缘计算是将数据处理和分析能力推向网络边缘的技术&#xff0c;使得终端设备能够实时、快速地响应环境变化&#xff0c;并做出相应决策。在智能制造中&#xff0c;通过5G工业网关的边缘计算能力&#xff0c;企业可以实现对生产线上大量传感器数据的实时采集、处理和分析&#…

Linux0715

一切皆文件&#xff0c;文件IO已经学习完毕&#xff0c;这两天完成一个minishell的小项目 文件操作 1. 标准IO 具有缓冲区 是对普通文件的读写 1. fopen ----------------------------->文件流指针 FILE * …

联发科又放大招,天玑9400支持10.7Gbps LPDDR5X内存性能拉满!

三星官方消息称&#xff0c;联发科天玑9400将支持全球最快的手机内存10.7Gbps LPDDR5X&#xff01;而且数码达人科技九州君也在微博上透露&#xff0c;天玑9400将首发支持全球最快的移动DRAM。顶级的内存加上天玑9400采用的黑鹰架构和配置的大CPU缓存&#xff0c;性能直接拉满了…

基于springboot和mybatis的RealWorld后端项目实战三之添加swagger

pom.xml添加依赖 <dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>2.9.2</version></dependency><dependency><groupId>io.springfox</groupId><arti…

基于python的层次聚类

目录 一、层次聚类概述 二、凝聚法&#xff08;Agglomerative Clustering&#xff09; 1. 初始化 2. 计算距离 3. 合并簇 4. 重复迭代 三、分裂法&#xff08;Divisive Clustering&#xff09; 1. 初始化 2. 分裂簇 3. 分配样本点 4. 重复迭代 四、其他考虑因素 五、总结 …

51、数据库的概念及sql语句

1、数据库 1.1、数据库管理&#xff1a; sql语句 数据库用来增删改查的语句。重要* 备份 数据库的数据进行备份。 主从复制&#xff0c;读写分离 高可用。重要*&#xff0c;原理–面试。 数据库的概念、语法和规范 1.2、数据库的定义 数据库&#xff1a;组织&#xff0c…

ACL实验

目录 一、实验拓扑​编辑 二. 实验要求&#xff08;在图中&#xff09; 三、实验思路 配IP 全网可达 创建模拟机pc1 创建telent r1 r2 由题目可得 截图 pcr1​编辑 pcr2​编辑 四、实验总结&#xff08;写实验完成后的总结心得&#xff09; 一、实验拓扑 二. 实验…

硅纪元AI应用推荐 | 精准识别用户意图,夸克真AI搜索引擎

“硅纪元AI应用推荐”栏目&#xff0c;为您精选最新、最实用的人工智能应用&#xff0c;无论您是AI发烧友还是新手&#xff0c;都能在这里找到提升生活和工作的利器。与我们一起探索AI的无限可能&#xff0c;开启智慧新时代&#xff01; 在数字化时代&#xff0c;搜索引擎成为我…

本地多模态看图说话-llava

其中图片为bast64转码&#xff0c;方便json序列化。 其中模型llava为本地ollama运行的模型&#xff0c;如&#xff1a;ollama run llava 还有其它的模型如&#xff1a;llava-phi3&#xff0c;通过phi3微调过的版本。 实际测试下来&#xff0c;发现本地多模型的性能不佳&…

EasyPoi一对多excel表格导出

效果如下图&#xff1a; 1、引入pom文件 <!--easypoi 一对多导入导出 --> <dependency><groupId>cn.afterturn</groupId><artifactId>easypoi-base</artifactId><version>4.2.0</version> </dependency> <dependenc…

AIGC降痕实战:论文降AI率的深度解析与应用

随着AI技术的飞速发展&#xff0c;AI论文工具正逐渐成为学术界的新宠。它们以高效、便捷的优势&#xff0c;吸引了众多学者的目光。然而&#xff0c;随之而来的学术诚信与原创性问题&#xff0c;也成为人们关注的焦点。 如何在享受AI带来的便利的同时&#xff0c;确保论文的原…

pear-admin-fast项目修改为集成PostgreSQL启动

全局搜索代码中的sysdate()&#xff0c;修改为now() 【前者是mysql特有的&#xff0c;后者是postgre特有的】修改application-dev.yml中的数据库url使用DBeaver把mysql中的数据库表导出csv&#xff0c;再从postgre中导入csv脚本转换后出现了bpchar(xx)类型&#xff0c;那么一定…

《数字通信世界》是什么级别的期刊?是正规期刊吗?能评职称吗?

​问题解答 问&#xff1a;《数字通信世界》是不是核心期刊&#xff1f; 答&#xff1a;不是&#xff0c;是知网收录的第二批认定学术期刊。 问&#xff1a;《数字通信世界》级别&#xff1f; 答&#xff1a;国家级。主管单位&#xff1a;工业和信息化部 主办单位&#x…