分布式锁:5种方案解决商品超卖的方案

2023-10-07 16:30

本文主要是介绍分布式锁:5种方案解决商品超卖的方案,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一 分布式锁

1.1 分布式锁的作用

在多线程高并发场景下,为了保证资源的线程安全问题,jdk为我们提供了synchronized关键字和ReentrantLock可重入锁,但是它们只能保证一个工程内的线程安全。在分布式集群、微服务、云原生横行的当下,如何保证不同进程、不同服务、不同机器的线程安全问题。jdk并没有给我们提供既有的解决方案。需要自己通过编写方案来解决,目前主流的实现有以下方式:

  1. 基于mysql关系型实现

  2. 基于redis非关系型数据实现

  3. 基于zookeeper/etcd实现

1.2  四种方案的比较

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

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

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

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

不推荐jvm本地锁。

二  模拟单体超卖

2.1 工程结构

2.2 编写工程代码

1.pom文件

  <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.46</version></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.4.1</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.16</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><version>2.3.12.RELEASE</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope><exclusions><exclusion><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId></exclusion></exclusions></dependency>

2.配置文件

server.port=9999
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/fenbu_lock?characterEncoding=utf-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=cloudiip
redis.host=172.16.116.100

 3.controller

@RestController
public class StockController {@Autowiredprivate StockService stockService;@GetMapping("stock/deduct")public String deduct(){// this.stockService.deduct();this.stockService.deductByMsqlDb();return "hello stock deduct!!";}}

4.service

@Service
public class StockService {@Autowiredprivate StockMapper stockMapper;private Stock stock = new Stock();private ReentrantLock lock = new ReentrantLock();public void deduct(){
//        lock.lock();
//        try {
//            stock.setStock(stock.getStock() - 1);
//            System.out.println("库存余量:" + stock.getStock());
//        } finally {
//            lock.unlock();
//        }}public void deductByMsqlDb(){// 先查询库存是否充足Stock stock = this.stockMapper.selectById(1L);// 再减库存if (stock != null && stock.getCount() > 0){stock.setCount(stock.getCount() - 1);this.stockMapper.updateById(stock);}}}

5.mapper


@Mapper
public interface StockMapper extends BaseMapper<Stock> {
}

6.启动类

@SpringBootApplication
@MapperScan("com.atguigu.distributed.lock.mapper")
public class DistributedLockApplication {public static void main(String[] args){SpringApplication.run(DistributedLockApplication.class, args);System.out.println("========================启动成功==========");}}

7.pojo类

@Data
@TableName("db_stock")
public class Stock {@TableIdprivate Long id;private String productCode;private String stockCode;private Integer count;// private Integer stock = 5000;
}

8.附件数据表

1.新建一个数据库,附件数据表,如图

2.脚本文件

CREATE TABLE `db_stock` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`product_code` varchar(255) DEFAULT NULL COMMENT '商品编号',`stock_code` varchar(255) DEFAULT NULL COMMENT '仓库编号',`count` int(11) DEFAULT NULL COMMENT '库存量',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

2.3 测试验证

http://localhost:9999/stock/deduct

查看数据库

2.4 jmeter模拟并发访问

2.4.1 启动jmeter

 2.4.2 配置jmeter

1.添加线程组

并发100循环50次,即5000次请求。  

3.给线程组添加HTTP Request请求:

4.将接口地址:http://localhost:9999/stock/deduct  配置到下面

 5.再选择你想要的测试报表,例如这里选择聚合报告:

启动测试,查看压力测试报告:

 参数api说明如下:

1.Label 取样器别名,如果勾选Include group name ,则会添加线程组的名称作为前缀

# Samples 取样器运行次数

Average 请求(事务)的平均响应时间

Median 中位数

90% Line 90%用户响应时间

95% Line 90%用户响应时间

99% Line 90%用户响应时间

Min 最小响应时间

Max 最大响应时间

Error 错误率

Throughput 吞吐率

Received KB/sec 每秒收到的千字节

Sent KB/sec 每秒收到的千字节

测试结果:请求总数5000次,平均请求时间129ms,中位数(50%)请求是在36ms内完成的,错误率0%,每秒钟平均吞吐量716.7次。

结论:此时如果还有人来下单,就会出现超卖现象(别人购买成功,而无货可发)。

三 方案1:使用jvm的本地锁解决冲突

3.1 原理

添加synchronized关键字之后,StockService就具备了对象锁,由于添加了独占的排他锁,同一时刻只有一个请求能够获取到锁,并减库存。此时,所有请求只会one-by-one执行下去,也就不会发生超卖现象。

3.2 操作

用jvm锁(synchronized关键字或者ReetrantLock)试试:

2.使用jmeter再次测试

查看数据库

并没有发生超卖现象,完美解决。  

3.3 此方案的缺点失效情况

1.多例模式  2.事务 ;3.集群

四 方案2:使用表级锁的sql解决冲突

4.1 表锁的使用范围

4.1.1 更新sql使用表锁

描述:

会话A执行: update db_stock set count=count-#{count} where product_code='1001' and count>=1

会话B执行:因为会话A执行的更新语句触发了表级锁,导致会话B无法执行插入,更新等语句。

insert into db_stock values(4,'1002','上海仓',5000);

update db_stock set count=count-1 where id=3;

1.会话A: 开启事务,执行更新语句,先不执行commit提交

2.会话B:  由于会话A执行更新语句后未提交,触发表级锁,此时自己进行更新,插入无法进行。

4.1.2 表锁变行锁

mysql悲观锁使用行级锁的条件:
1.锁的查询或者更新必须使用索引字段
2.查询或者更新必须是具体值。

1.给查询条件设置索引字段,让更新语句变为行级锁

2.会话A执行更新,让更新语句变为行级锁

3.会话B进行更新,回车后,可以看到进行提交执行了 

4.2 操作案例 

1.mapper级别

    @Update("update db_stock set count=count-#{count} where product_code=#{productCode} and count>=#{count}")int updateStock(@Param("productCode") String productCode,@Param("count") Integer count);

2.service

  public   void deductBySql(){// 先查询库存是否充足this.stockMapper.updateStock("1001",1);System.out.println("请求进来了.......");}

 3.查看数据库

4.并发压力测试

5.查看效果:均正确消费。

4.3 此方案缺点

优点:能够解决jvm本地锁多失效的3种情况。

缺点:1.确定锁的范围 行级锁还是表级锁;2.同一个商品有多条库存记录;

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

五  方案3:使用悲观锁解决冲突

5.1 使用悲观锁原理

除了使用jvm锁之外,还可以使用数据锁:悲观锁 或者 乐观锁。

1.悲观锁:在select的时候就会加锁,采用先加锁后处理的模式,虽然保证了数据处理的安全性,但也会阻塞其他线程的写操作。在读取数据时锁住那几行,其他对这几行的更新需要等到悲观锁结束时才能继续 。select ... for update
悲观锁适用于写多读少的场景,因为拿不到锁的线程,会将线程挂起,交出CPU资源,可以把CPU给其他线程使用,提高了CPU的利用率。

会话A:select ... for update   给具体的行数据加上排他锁,也即行锁。

会话B :无法对1001进行更新,因为上了行级锁

5.2 操作案例

一个sql:直接更新时判断,在更新中判断库存是否大于0 ;

update table set surplus = (surplus - buyQuantity) where id = 1 and (surplus - buyQuantity) > 0

1.mapper:编写悲观锁语句

2.service:添加事务注解  @Transactional

3.数据表

4.jmeter压力测试

 5.查看效果:成功实现所减数据为0,均正确消费。

5.3 此方案的优缺点

1.性能问题;2.死锁问题:对多条数据加锁时,加锁顺序要一致;

3.库存操作要统一,一个会话用 select  x for update  一个会话执行select可以进行查询 ,存在数据不一致情况。

会话A:进行查询上表锁

会话B:可以进行查询查询。

六  方案4:使用乐观锁解决冲突

6.1 乐观锁原理

乐观锁:采取了更加宽松的加锁机制,大多是基于数据版本( Version )及时间戳来实现。适合于读比较多,不会阻塞读,读取数据时不上锁,更新时检查是否数据已经被更新过,如果是则取消当前更新进行重试。version 或者 时间戳(CAS思想)。

6.2 操作案例

使用数据版本(Version)记录机制实现,这是乐观锁最常用的实现 方式。一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录 的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新。

更新sql:

select * from db_stock where product_code='1001'
update db_stock set count=4996,version=version+1 where id=1 and version=0;

1.修改service

2.数据库表

3.压力测试

4.查看消费结果: 均正确消费

6.3 乐观锁存在的缺点

1.高并发情况下,性能比较低下,并发量越小,性能越高。 2.读写情况下,乐观锁不可靠。

七 方案5:使用redis的乐观锁

7.1 redis的乐观锁的原理

这篇关于分布式锁:5种方案解决商品超卖的方案的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/158983

相关文章

C++右移运算符的一个小坑及解决

《C++右移运算符的一个小坑及解决》文章指出右移运算符处理负数时左侧补1导致死循环,与除法行为不同,强调需注意补码机制以正确统计二进制1的个数... 目录我遇到了这么一个www.chinasem.cn函数由此可以看到也很好理解总结我遇到了这么一个函数template<typename T>unsigned

MyBatis Plus实现时间字段自动填充的完整方案

《MyBatisPlus实现时间字段自动填充的完整方案》在日常开发中,我们经常需要记录数据的创建时间和更新时间,传统的做法是在每次插入或更新操作时手动设置这些时间字段,这种方式不仅繁琐,还容易遗漏,... 目录前言解决目标技术栈实现步骤1. 实体类注解配置2. 创建元数据处理器3. 服务层代码优化填充机制详

防止Linux rm命令误操作的多场景防护方案与实践

《防止Linuxrm命令误操作的多场景防护方案与实践》在Linux系统中,rm命令是删除文件和目录的高效工具,但一旦误操作,如执行rm-rf/或rm-rf/*,极易导致系统数据灾难,本文针对不同场景... 目录引言理解 rm 命令及误操作风险rm 命令基础常见误操作案例防护方案使用 rm编程 别名及安全删除

Python实现批量CSV转Excel的高性能处理方案

《Python实现批量CSV转Excel的高性能处理方案》在日常办公中,我们经常需要将CSV格式的数据转换为Excel文件,本文将介绍一个基于Python的高性能解决方案,感兴趣的小伙伴可以跟随小编一... 目录一、场景需求二、技术方案三、核心代码四、批量处理方案五、性能优化六、使用示例完整代码七、小结一、

C#使用Spire.Doc for .NET实现HTML转Word的高效方案

《C#使用Spire.Docfor.NET实现HTML转Word的高效方案》在Web开发中,HTML内容的生成与处理是高频需求,然而,当用户需要将HTML页面或动态生成的HTML字符串转换为Wor... 目录引言一、html转Word的典型场景与挑战二、用 Spire.Doc 实现 HTML 转 Word1

使用Python实现Word文档的自动化对比方案

《使用Python实现Word文档的自动化对比方案》我们经常需要比较两个Word文档的版本差异,无论是合同修订、论文修改还是代码文档更新,人工比对不仅效率低下,还容易遗漏关键改动,下面通过一个实际案例... 目录引言一、使用python-docx库解析文档结构二、使用difflib进行差异比对三、高级对比方

Three.js构建一个 3D 商品展示空间完整实战项目

《Three.js构建一个3D商品展示空间完整实战项目》Three.js是一个强大的JavaScript库,专用于在Web浏览器中创建3D图形,:本文主要介绍Three.js构建一个3D商品展... 目录引言项目核心技术1. 项目架构与资源组织2. 多模型切换、交互热点绑定3. 移动端适配与帧率优化4. 可

504 Gateway Timeout网关超时的根源及完美解决方法

《504GatewayTimeout网关超时的根源及完美解决方法》在日常开发和运维过程中,504GatewayTimeout错误是常见的网络问题之一,尤其是在使用反向代理(如Nginx)或... 目录引言为什么会出现 504 错误?1. 探索 504 Gateway Timeout 错误的根源 1.1 后端

解决升级JDK报错:module java.base does not“opens java.lang.reflect“to unnamed module问题

《解决升级JDK报错:modulejava.basedoesnot“opensjava.lang.reflect“tounnamedmodule问题》SpringBoot启动错误源于Jav... 目录问题描述原因分析解决方案总结问题描述启动sprintboot时报以下错误原因分析编程异js常是由Ja

深度剖析SpringBoot日志性能提升的原因与解决

《深度剖析SpringBoot日志性能提升的原因与解决》日志记录本该是辅助工具,却为何成了性能瓶颈,SpringBoot如何用代码彻底破解日志导致的高延迟问题,感兴趣的小伙伴可以跟随小编一起学习一下... 目录前言第一章:日志性能陷阱的底层原理1.1 日志级别的“双刃剑”效应1.2 同步日志的“吞吐量杀手”