(三)库存超卖案例实战——使用redis分布式锁解决“超卖”问题

news/2024/4/21 0:26:51/

前言

在上一节内容中我们介绍了如何使用mysql数据库的传统锁(行锁、乐观锁、悲观锁)来解决并发访问导致的“超卖问题”。虽然mysql的传统锁能够很好的解决并发访问的问题,但是从性能上来讲,mysql的表现似乎并不那么优秀,而且会受制于单点故障。本节内容我们介绍一种性能更加优良的解决方案,使用内存数据库redis实现分布式锁从而控制并发访问导致的“超卖”问题。关于redis环境的搭建这里不做介绍,可查看作者往期博客内容。

正文

  • 在项目中添加redis的依赖和配置信息

- pom依赖配置

<!--        数据库连接池工具包-->
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId>
</dependency><!--redis启动器-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

- application.yml配置

spring:application:name: ht-atp-platdatasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://192.168.110.88:3306/ht-atp?characterEncoding=utf-8&serverTimezone=GMT%2B8&useAffectedRows=true&nullCatalogMeansCurrent=trueusername: rootpassword: rootprofiles:active: dev# redis配置redis:host: 192.168.110.88lettuce:pool:# 连接池最大连接数(使用负值表示没有限制) 默认为8max-active: 8# 连接池中的最小空闲连接 默认为 0min-idle: 1# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认为-1max-wait: 1000# 连接池中的最大空闲连接 默认为8max-idle: 8

- redis序列化配置

package com.ht.atp.plat.config;import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;@Configuration
public class RedisConfig {/*** @param factory* @return*/@Beanpublic RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {// 缓存序列化配置,避免存储乱码RedisTemplate<String, Object> template = new RedisTemplate<>();template.setConnectionFactory(factory);Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);ObjectMapper objectMapper = new ObjectMapper();objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);jackson2JsonRedisSerializer.setObjectMapper(objectMapper);StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();// key采用String的序列化方式template.setKeySerializer(stringRedisSerializer);// hash的key也采用String的序列化方式template.setHashKeySerializer(stringRedisSerializer);// value序列化方式采用jacksontemplate.setValueSerializer(jackson2JsonRedisSerializer);// hash的value序列化方式采用jacksontemplate.setHashValueSerializer(jackson2JsonRedisSerializer);template.afterPropertiesSet();return template;}
}

  •  在redis中增加商品P0001的库存数量为10000

  • 使用redis不加锁的业务测试

- 业务测试代码

    /*** 使用redis不加锁*/@Overridepublic void checkAndReduceStock() {// 1. 查询库存数量String stockQuantity = redisTemplate.opsForValue().get("P0001").toString();// 2. 判断库存是否充足if (stockQuantity != null && stockQuantity.length() != 0) {Integer quantity = Integer.valueOf(stockQuantity);if (quantity > 0) {// 3.扣减库存redisTemplate.opsForValue().set("P0001", String.valueOf(--quantity));}}}

- 使用jmeter压测,查看测试结果:库存并没有减少为0,说明存在“超卖”问题

  • 使用redis的setnx指令加锁,开启三个相同服务,使用jmeter压测

- redis加锁测试代码

/*** 使用redis加锁* */@Overridepublic void checkAndReduceStock() {// 1.使用setnx加锁Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock-stock", "0000");// 2.重试:递归调用,如果获取不到锁if (!lock) {try {//暂停50msThread.sleep(50);this.checkAndReduceStock();} catch (InterruptedException e) {e.printStackTrace();}} else {try {// 3. 查询库存数量String stockQuantity = (String) redisTemplate.opsForValue().get("P0001");// 4. 判断库存是否充足if (stockQuantity != null && stockQuantity.length() != 0) {Integer quantity = Integer.valueOf(stockQuantity);if (quantity > 0) {// 5.扣减库存redisTemplate.opsForValue().set("P0001", String.valueOf(--quantity));}} else {System.out.println("该库存不存在!");}} finally {// 5.解锁redisTemplate.delete("lock-stock");}}}

- 开启服务7000、7001、7002

 - jmeter压测结果:平均访问时间364ms,接口吞吐量为每秒249

- redis数据库库存结果为:0,并发“超卖”问题解决

  • 以上普通加锁方式存在死锁问题及死锁问题的解决方案

- 死锁产生的原因:在上述redis加锁的正常情况下,是可以解决并发访问的问题,但是也存在死锁的问题,例如7000的服务获取到锁之后,由于服务异常导致锁没有释放,那么7001和7002服务将永远不可能获取到锁。

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

①使用expire设置过期时间(缺乏原子性:如果在setnx和expire之间出现异常,锁也无法释放)

②使用setex指令设置过期时间:set key value ex 3 nx(保证原子性操作既达到setnx的效果,又设置了过期时间)

- 代码实现

public void checkAndReduceStock() {// 1.使用setex加锁,保证加锁的原子性,以及锁可以自动释放Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock-stock", "0000",3, TimeUnit.SECONDS);// 2.重试:递归调用,如果获取不到锁if (!lock) {try {//暂停50msThread.sleep(50);this.checkAndReduceStock();} catch (InterruptedException e) {e.printStackTrace();}} else {try {// 3. 查询库存数量String stockQuantity = (String) redisTemplate.opsForValue().get("P0001");// 4. 判断库存是否充足if (stockQuantity != null && stockQuantity.length() != 0) {Integer quantity = Integer.valueOf(stockQuantity);if (quantity > 0) {// 5.扣减库存redisTemplate.opsForValue().set("P0001", String.valueOf(--quantity));}} else {System.out.println("该库存不存在!");}} finally {// 5.解锁redisTemplate.delete("lock-stock");}}}

- 测试结果:库存扣减为0,锁也释放

  •  防止误删,在以上普通加锁的方式下,存在锁被误删除的情况

- 锁误删除的原因:在上面的加锁场景中,会出现以下的情况,A请求方法获取到锁之后,在业务还没有执行完成,锁就被自动释放,这个时候B请求方法也会获取到锁,在B业务还未执行完成之前,A执行完成并执行手动删除锁操作,这个时候会把B业务的锁释放掉,导致B刚刚获取到锁就被释放,从而产生后续的并发访问问题。

- 模拟锁误删除产生的并发问题

- 库存扣减结果:没有扣减为0,产生并发问题

- 解决方案,每个请求使用全局唯一UUID为value值,删除锁之前,先判断value值是否相同,相同再删除锁

public void checkAndReduceStock() {// 1.使用setex加锁,保证加锁的原子性,以及锁可以自动释放String uuid = UUID.randomUUID().toString();Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock-stock", uuid, 1, TimeUnit.SECONDS);// 2.重试:递归调用,如果获取不到锁if (!lock) {try {//暂停50msThread.sleep(10);this.checkAndReduceStock();} catch (InterruptedException e) {e.printStackTrace();}} else {try {// 3. 查询库存数量String stockQuantity = (String) redisTemplate.opsForValue().get("P0001");// 4. 判断库存是否充足if (stockQuantity != null && stockQuantity.length() != 0) {Integer quantity = Integer.valueOf(stockQuantity);if (quantity > 0) {// 5.扣减库存redisTemplate.opsForValue().set("P0001", String.valueOf(--quantity));}} else {System.out.println("该库存不存在!");}} finally {// 5.先判断是否是自己的锁,然后再解锁String redisUuid = (String) redisTemplate.opsForValue().get("lock-stock");if (StringUtils.equals(uuid, redisUuid)) {redisTemplate.delete("lock-stock");}}}}

- 存在的问题:由于判断锁和解锁的操作不具有原子性,仍然会存在误删除的操作,如A请求在完成判断之后准备删除锁的时候,此时A的锁自动释放,B请求获取到锁,这个时候A请求会手动将B请求的锁删除掉,依然存在并发访问的问题。该概率很小。

  •  使用lua脚本解决锁手动释放删除的操作是原子性操作

- lua代码解决误删操作

public void checkAndReduceStock() {// 1.使用setex加锁,保证加锁的原子性,以及锁可以自动释放String uuid = UUID.randomUUID().toString();Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock-stock", uuid, 1, TimeUnit.SECONDS);// 2.重试:递归调用,如果获取不到锁if (!lock) {try {//暂停50msThread.sleep(10);this.checkAndReduceStock();} catch (InterruptedException e) {e.printStackTrace();}} else {try {// 3. 查询库存数量String stockQuantity = (String) redisTemplate.opsForValue().get("P0001");// 4. 判断库存是否充足if (stockQuantity != null && stockQuantity.length() != 0) {Integer quantity = Integer.valueOf(stockQuantity);if (quantity > 0) {// 5.扣减库存redisTemplate.opsForValue().set("P0001", String.valueOf(--quantity));}} else {System.out.println("该库存不存在!");}} finally {// 5.先判断是否是自己的锁,然后再解锁String script = "if redis.call('get', KEYS[1]) == ARGV[1] " +"then " +"   return redis.call('del', KEYS[1]) " +"else " +"   return 0 " +"end";redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList("lock-stock"), uuid);}}}

结语

关于使用redis分布式锁解决“超卖”问题的内容到这里就结束了,我们下期见。。。。。。


http://www.ppmy.cn/news/1175730.html

相关文章

聊聊分布式架构09——分布式中的一致性协议

目录 01从集中式到分布式 系统特点 集中式特点 分布式特点 事务处理差异 02一致性协议与Paxos算法 2PC&#xff08;Two-Phase Commit&#xff09; 阶段一&#xff1a;提交事务请求 阶段二&#xff1a;执行事务提交 优缺点 3PC&#xff08;Three-Phase Commit&#x…

药房商城小程序便捷购药体验,找药买药一键操作

在我们的生活中&#xff0c;偶尔会遇到身体不适或需要常见药品的情况。然而&#xff0c;传统的购药方式往往会浪费大量的时间和精力。幸运的是&#xff0c;现在有了药房商城小程序&#xff0c;仅需一键操作&#xff0c;您就能享受到便捷、高效的购药体验。 药房商城小程序是用于…

经典链表试题(一)

文章目录 一、两数相加1、题目介绍2、思路讲解3、代码实现 二、合并两个有序链表1、题目介绍2、思路讲解3、代码实现 三、环形链表&#xff08;二&#xff09;1、题目介绍2、思路讲解3、代码实现 四、环形链表&#xff08;一&#xff09;1、题目介绍2、思路讲解3、代码实现 五、…

STM32-ADC实验

目录 实验1&#xff1a;单ADC单通道中断 硬件原理图 USART配置 ADC1配置 初始化结构体的参数 ScanConvMode&#xff1a;扫描转换模式 ContinuousConvMode&#xff1a;连续转换模式 ExternalTrigConv&#xff1a;外部触发方式 测试环节 实验现象 实验2&#xff1a;单…

算法通过村第十六关-滑动窗口|白银笔记|经典题目讲解

文章目录 前言最长字串专场无重复字符的最长字串至多包含两个不同字串的最长子串至多包含K个不同字串的最长子串 长度最小的子数组盛水最多的容器寻找字串异位词(排序)字符串的排序找到字符串中所有字母异位 总结 前言 提示&#xff1a;所有的话语都颇为类似&#xff0c;而沉默…

【Java 进阶篇】Java XML约束:确保数据一致性和有效性

XML&#xff08;可扩展标记语言&#xff09;是一种常用的数据交换格式&#xff0c;用于存储和交换数据。然而&#xff0c;为了确保数据的一致性和有效性&#xff0c;通常需要定义XML约束。XML约束是一种规则集&#xff0c;定义了XML文档的结构、元素、属性和数据类型。本篇博客…

所求和问题

&#xff08; 1 2 3 ) 2 1 2 2 2 3 2 2 ∗ ( 1 ∗ 2 1 ∗ 3 2 ∗ 3 ) &#xff08;123)^21^22^23^22*(1*21*32*3) &#xff08;123)21222322∗(1∗21∗32∗3) n input() a list(map(int,input().split())) s1 sum(a) ** 2 s2 0 for i in a:s2 i ** 2 ans (s1 …

CPSC法规-2023/9月召回案例 多款产品被召回

国内双十一大促的预热已然开始&#xff0c;国外感恩节、万圣节、黑五等产品大促也随即临近&#xff0c;卖家在大量备货的同时&#xff0c;务必保障自身产品通过相关安全测试&#xff0c;一旦产品被相关部门抽查没能达标&#xff0c;或因质量问题被消费者投诉&#xff0c;将可能…

JAVA 短剧系统小程序的开发流程

随着移动互联网的普及&#xff0c;小程序成为了炙手可热的应用形态。其中&#xff0c;JAVA短剧系统小程序以其独特的魅力&#xff0c;在市场上占据了一席之地。本文将详细介绍JAVA短剧系统小程序的开发流程及技术实现。 一、开发背景和意义 JAVA短剧系统小程序是一种基于JAVA…

C语言解决八皇后问题

八皇后问题是指在一个88的棋盘上&#xff0c;放置8个皇后&#xff0c;使得任意两个皇后都不能在同一行、同一列或同一斜线上。这是一个著名的递归问题。下面是一个C语言实现八皇后问题的代码&#xff0c;以及对代码的讲解。 #include <stdio.h>int board[8][8] {0}; //…

ChineseChess2

中国象棋&#xff1a;黑将&#xff0c;红帅双炮&#xff0c;只要红帅中间露头怎么走怎么赢 卡主黑将的走位&#xff0c;控制住就好了 ChineseChess-CSDN博客

运维部署宝典-长更版

亲测、简洁、长更 防火墙操作&#xff1a;安装vm16&#xff1a;安装centos&#xff08;重点&#xff1a;重启ip会替换&#xff09;:安装redis&#xff1a;主从复制&#xff1a;哨兵模式&#xff1a; java安装&#xff1a;安装nginx&#xff1a;elkf搭建&#xff1a;elasticsear…

HTTP 协议参考文档

开发者Web协议文档&#xff1a; https://developer.mozilla.org/zh-CN/docs/Web 其中子节点包含 HTTP 协议内容&#xff1a; https://developer.mozilla.org/zh-CN/docs/Web/HTTP 其内容是基于 markdown 编写的&#xff0c;对应源文件在 Github 中&#xff0c;如下&#xf…

解决netty作为web,post请求体过大导致413 Request Entity Too Largew问题

问题 项目中使用netty作为web服务&#xff0c;postman请求体内容超出5mb请求netty时&#xff0c;返回413 Request Entity Too Large 解决 查询了一下资料&#xff1a;https://netty.io/4.0/api/io/netty/handler/codec/http/HttpObjectAggregator.html ChannelPipeline p .…

update:将另一表里查询的数据,拼接赋值

在公司业务里&#xff0c;需要在存储过程里&#xff0c;更新操作&#xff1a;将另一表里查询的数据&#xff0c;拼接赋值 UPDATE aSET a.cdescription申请于b.dneed前采购CONVERT(VARCHAR(10),icount)种物品FROM op_engorder_e_spe a,(SELECT COUNT(1) icount,MAX(CONVERT(VARC…

jenkins配置gitlab凭据

下载Credentials Binding插件&#xff08;默认是已经安装了&#xff09; 在凭据配置里添加凭据类型 点击保存 Username with password&#xff1a; 用户名和密码 SSH Username with private 在凭据管理里面添加gitlab账号和密码 点击全局 点击添加凭据&#xff08;版本不同…

基于自动化工具autox.js的抢票(猫眼)

1.看到朋友圈抢周杰伦、林俊杰演唱会票贼难信息,特研究了一段时间,用autox.js写了自动化抢票脚本,购票页面自动点击下单(仅限安卓手机)。 2.脚本运行图 3.前期准备工作 (1)autox.js社区官网:AutoX.js (2)b站上学习资料:10分钟学会AutoX.js hello world_哔哩哔哩_bi…

《 汇编语言的系统学习》一、编程语言、机器语言与汇编语言

目录 《 汇编语言的系统学习》1、编程语言1.1 语言1.2 程序 2 编程语言分类2.1 机器语言2.2 汇编语言2.3 高级程序语言2.3.1 编译型2.3.1 解释型 《 汇编语言的系统学习》 1、编程语言 1.1 语言 定义&#xff1a;一种系统的&#xff0c;人与人之间通过声音、符号等进行交流的…

智能振弦传感器:参数智能识别技术的重要科技创新

智能振弦传感器&#xff1a;参数智能识别技术的重要科技创新 智能振弦传感器是一种能够自动识别传感器参数的高科技产品。它的研发得益于河北稳控科技的不断创新和努力&#xff0c;其电子标签专用读数模块模块TR01将传感器生产和标定过程实现了自动化。该模块将温度电阻两芯线…

还原现场——前端录制用户行为技术方案

一、问题背景 目前&#xff0c;在我们的项目中通常会使用各种各样的埋点和监控来收集页面访问的信息&#xff0c;例如点击埋点、PV埋点等&#xff0c;这些埋点数据能够反应绝大部分的用户行为&#xff0c;但是对于一些关注上下文的使用场景而言这些埋点是不够的。 对于产品而言…