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

本文从一个经典的库存超卖问题分析说明常见锁的应用,假设库存资源存储在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() {@Overridepublic 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.mzph.cn/web/45721.shtml

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

相关文章

JavaSE从零开始到精通

1.前置知识 JVM&#xff1a;java virtrual machine, java虚拟机, 专门用于执行java代码的一款软件。JRE&#xff1a;java runtime enviroment, java运行时环境, java官方提供的核心类库. jre中包含了核心类库和jvm。JDK: java development kit, java开发工具包, javac.exe, ja…

LVS+Keepalive高可用

1、keepalive 调度器的高可用 vip地址主备之间的切换&#xff0c;主在工作时&#xff0c;vip地址只在主上&#xff0c;vip漂移到备服务器。 在主备的优先级不变的情况下&#xff0c;主恢复工作&#xff0c;vip会飘回到住服务器 1、配优先级 2、配置vip和真实服务器 3、主…

我想做信号通路分析,但我就是不想学编程

“我想做信号通路分析&#xff0c;但我就是不想学编程。” “我又不是生信狗&#xff0c;学代码会死。” “你们这些做生信的&#xff0c;整天把数据分析搞得神神秘秘&#xff0c;不就是怕被人抢饭碗而已嘛。” “这都没分析出我想要的结果&#xff0c;不靠谱。” “你们做…

【自学安全防御】二、防火墙NAT智能选路综合实验

任务要求&#xff1a; &#xff08;衔接上一个实验所以从第七点开始&#xff0c;但与上一个实验关系不大&#xff09; 7&#xff0c;办公区设备可以通过电信链路和移动链路上网(多对多的NAT&#xff0c;并且需要保留一个公网IP不能用来转换) 8&#xff0c;分公司设备可以通过总…

Java爬虫安全策略:防止TikTok音频抓取过程中的请求被拦截

摘要 在当今互联网时代&#xff0c;数据采集已成为获取信息的重要手段。然而&#xff0c;随着反爬虫技术的不断进步&#xff0c;爬虫开发者面临着越来越多的挑战。本文将探讨Java爬虫在抓取TikTok音频时的安全策略&#xff0c;包括如何防止请求被拦截&#xff0c;以及如何提高…

Andriod Stdio新建Kotlin的Jetpack Compose简单项目

1.选择 No Activity 2.选择kotlin 4.右键选择 在目录MyApplication下 New->Compose->Empty Project 出现下面的画面 Finish 完成

C++——类和对象(中)

文章目录 一、类的默认成员函数二、构造函数三、析构函数四、拷⻉构造函数五、赋值运算符重载1. 运算符重载2. 赋值运算符重载 六、取地址运算符重载const成员函数取地址运算符重载 七、应用&#xff1a;⽇期类实现Date.hDate.cpptest.cpp 一、类的默认成员函数 默认成员函数就…

技术成神之路:设计模式(七)状态模式

1.介绍 状态模式&#xff08;State Pattern&#xff09;是一种行为设计模式&#xff0c;它允许一个对象在其内部状态改变时改变其行为。这个模式将状态的相关行为封装在独立的状态类中&#xff0c;并将不同状态之间的转换逻辑分离开来。 2.主要作用 状态模式的主要作用是让一个…

数据结构—链式二叉树-C语言

代码位置&#xff1a;test-c-2024: 对C语言习题代码的练习 (gitee.com) 一、前言&#xff1a; 在现实中搜索二叉树为常用的二叉树之一&#xff0c;今天我们就要通过链表来实现搜索二叉树。实现的操作有&#xff1a;建二叉树、前序遍历、中序遍历、后序遍历、求树的节点个数、求…

MySQL日期和时间相关函数

目录 1. 获取当前时间和日期 2. 获取当前日期 3. 获取当前时间 4. 获取单独的年/月/日/时/分/秒 5. 添加时间间隔 date_add ( ) 6. 格式化日期 date_format ( ) 7. 字符串转日期 str_to_date () 8. 第几天 dayofxx 9. 当月最后一天 last_day ( ) 10. 日期差 datedif…

H. Beppa and SwerChat【双指针】

思路分析&#xff1a;运用双指针从后往前扫一遍&#xff0c;两次分别记作数组a&#xff0c;b&#xff0c;分别使用双指针i和j来扫&#xff0c;如果一样就往前&#xff0c;如果不一样&#xff0c;i–,ans #include<iostream> #include<cstring> #include<string…

SQL server 练习题2

课后作业 作业 1&#xff1a;自己查找方法&#xff0c;将 homework_1.xls 文件数据导入到 SQLServer 的 homework 数据库中。数据导入完成后&#xff0c;把表名统一改为&#xff1a;外卖表 如下所示&#xff1a; 作业 2&#xff1a;找出所有在 2020 年 5 月 1 日至 5 月 31 …

Zookeeper之CAP理论及分布式一致性算法

CAP理论 CAP理论告诉我们&#xff0c;一个分布式系统不可能同时满足以下三种 一致性&#xff08;C:consistency&#xff09;可用性&#xff08;A:Available&#xff09;分区容错性&#xff08;P:Partition Tolerance&#xff09; 这三个基本要求&#xff0c;最多只能同时满足…

部署k8s 1.28.9版本

继上篇通过vagrant与virtualBox实现虚拟机的安装。笔者已经将原有的vmware版本的虚拟机卸载掉了。这个场景下&#xff0c;需要重新安装k8s 相关组件。由于之前写的一篇文章本身也没有截图。只有命令。所以趁着现在。写一篇&#xff0c;完整版带截图的步骤。现在行业这么卷。离…

SpringBoot中常用的注解及其用法

1. 常用类注解 RestController和Controller是Spring中用于定义控制器的两个类注解. 1.1 RestController RestController是一个组合类注解,是Controller和ResponseBody两个注解的组合,在使 用 RestController 注解标记的类中&#xff0c;每个方法的返回值都会以 JSON 或 XML…

【Android安全】Ubuntu 下载、编译 、刷入Android-8.1.0_r1

0. 环境准备 Ubuntu 16.04 LTS&#xff08;预留至少95GB磁盘空间&#xff0c;实测占94.2GB&#xff09; Pixel 2 XL 要买欧版的&#xff0c;不要美版的。 欧版能解锁BootLoader、能刷机。 美版IMEI里一般带“v”或者"version"&#xff0c;这样不能解锁BootLoader、…

394. 字符串解码 739. 每日温度(LeetCode热题100)

394. 字符串解码 - 力扣&#xff08;LeetCode&#xff09; curr_str&#xff1a;遍历整个字符串时 如果左边有[&#xff0c;且无相应右括号和其匹配&#xff0c;那么curr_str就表示该[到当前位置的解码字符串如果左边的[]已经匹配&#xff0c;或者没有[]&#xff0c;curr_siz…

找不到vcruntime140_1.dll 无法执行的相关解决方法,如何高效率修复vcruntime140_1.dll

当出现“找不到 vcruntime140_1.dll 无法执行”这类提示时&#xff0c;意味着你的系统中的 vcruntime140_1.dll 文件已经缺失或者损坏。为了恢复并正常启动你的程序&#xff0c;你需要对这个 DLL 文件进行修复。接下来&#xff0c;我们将详细介绍如何进行这一操作。 一.找不到v…

数学建模·层次分析法

层次分析法 LAF 定义 评价体系的优劣影响&#xff0c;计算评价指标的权重的一种方法 主观性较强&#xff0c;现在一般不用 主要步骤 关键在于一致性检验和求权值 权重的计算 注意权重之和为1&#xff0c;需要归一化 算数平均法 特征值法 矩阵的一致性检验 为什么要检验…

sentinel网关限流配置及使用

sentinel控制台源码&#xff1a;https://download.csdn.net/download/yixin605691235/89543923 sentinel控制台jar包&#xff1a;https://download.csdn.net/download/yixin605691235/89543931 不同环境直接修改jar包中的application.yml文件中的nacos地址就可以了。 一、网关限…