第一章_从减库存聊起

news2024/12/24 20:20:15
在多线程高并发场景下,为了保证资源的线程安全问题, jdk 为我们提供了 synchronized 关键字和
ReentrantLock 可重入锁,但是它们只能保证一个 jvm 内的线程安全。在分布式集群、微服务、云原生横行的当下,如何保证不同进程、不同服务、不同机器的线程安全问题, jdk 并没有给我们提供既有的 解决方案。此时,我们就必须借助于相关技术手动实现了。目前主流的实现有三种方式:
  1.  基于mysql关系型实现
  2.  基于redis非关系型数据实现
  3.  基于zookeeper实现
从减库存聊起
库存在并发量较大情况下很容易发生超卖现象,一旦发生超卖现象,就会出现多成交了订单而发不了货的情况。
场景:
商品 S 库存余量为 5 时,用户 A B 同时来购买一个商品 S ,此时查询库存数都为 5 ,库存充足则开始
减库存:
用户 A update db_stock set stock = stock - 1 where id = 1
用户 B update db_stock set stock = stock - 1 where id = 1
并发情况下,更新后的结果可能是 4 ,而实际的最终库存量应该是 3 才对
环境准备
建表语句:
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=2 DEFAULT CHARSET=utf8;
表中数据如下:

1001商品在001仓库有5000件库存。

创建分布式锁demo工程:

创建好之后:

pom.xml 如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.lzx</groupId>
    <artifactId>distributed_lock</artifactId>
    <version>1.0</version>

    <properties>
        <java.version>1.8</java.version>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.0</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>
        </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>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
application.yml 配置文件:
server:
  port: 6000

spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://172.16.116.100:3306/test
    username: root
    password: root
  redis:
    host: 172.16.116.100
DistributedLockApplication 启动类:
@SpringBootApplication
@MapperScan("com.lzx.distributedlock.mapper")
public class DistributedLockApplication {

    public static void main(String[] args) {
       SpringApplication.run(DistributedLockApplication.class, args);
    }

}
Stock 实体类:
@Data
@TableName("db_stock")
public class Stock {

    @TableId
    private Long id;

    private String productCode;

    private String stockCode;

    private Integer count;

}
StockMapper 接口:
public interface StockMapper extends BaseMapper<Stock> {

}
简单实现减库存
接下来代码实操一下

StockController

@RestController
public class StockController {

   @Autowired
   private StockService stockService;

   @GetMapping("/check/lock")
   public String checkAndLock(){
       this.stockService.checkAndLock();
       return "验库存并锁库存成功!";
   }

}
StockService
@Service
public class StockService {

    @Autowired
    private StockMapper stockMapper;

    public void checkAndLock() {
        // 先查询库存是否充足
        Stock stock = this.stockMapper.selectById(1L);

        // 再减库存
        if (stock != null && stock.getCount() > 0) {
            stock.setCount(stock.getCount() - 1);
            this.stockMapper.updateById(stock);
        }

    }
}
测试:
查看数据库:
在浏览器中一个一个访问时,每访问一次,库存量减 1 ,没有任何问题。
演示超卖现象
接下来使用 jmeter 压力测试工具,高并发下压测一下,添加线程组:并发 100 循环 50 次,即 5000
请求。

给线程组添加HTTP Request请求: 

填写测试接口路径如下:
再选择你想要的测试报表,例如这里选择聚合报告:
启动测试,查看压力测试报告:
测试结果:请求总数 5000 次,平均请求时间 202ms ,中位数( 50% )请求是在 173ms 内完成的, 90%请求是在 344ms 内完成的,最小耗时 12ms ,最大耗时 1125ms ,错误率 0% ,每秒钟平均 473.8 次。
查看 mysql 数据库剩余库存数:还有 4870
此时如果还有人来下单,就会出现超卖现象(别人购买成功,而无货可发)。
jvm 锁问题演示
添加 jvm
使用 jvm 锁( synchronized 关键字或者 ReetrantLock )试试:
重启 tomcat 服务,再次使用 jmeter 压力测试,效果如下:
查看 mysql 数据库:
并没有发生超卖现象,完美解决。
原理
添加 synchronized 关键字之后, StockService 就具备了对象锁,由于添加了独占的排他锁,同一时刻只有一个请求能够获取到锁,并减库存。此时,所有请求只会 one-by-one 执行下去,也就不会发生超卖现 象。
事务问题
修改库存扣减方法,在上面添加事务注解试试:

测试结果

又出现了问题

原因分析

由于springboot事务是基于aop实现的,mysql的默认事务隔离级别为RR,可重复读,假设当前有两个线程同时并发

最终导致库存更新出现问题。

下面修改事务隔离级别为读未提交

测试结果

并没有发生超卖现象,解决了问题;但是一般不会这么做,因为在真实的库存扣减中通常伴随着订单等多表的操作,不单单只是库存表,可能会引起其他的问题。

多例问题

测试结果

多例模式下,库存扣减也出现了问题,与下面的多服务问题类似。

多服务问题
使用 jvm 锁在单工程单服务情况下确实没有问题,但是在集群情况下会怎样?
接下启动多个服务并使用 nginx 负载均衡,结构如下:

启动三个服务(端口号分别8000 8100 8200),如下:

安装配置nginx 

基于安装 nginx
#拉取镜像
docker pull nginx:latest

#创建nginx对应资源、日志及配置目录
mkdir -p /opt/nginx/logs /opt/nginx/conf /opt/nginx/html

#先在conf目录下创建nginx.conf文件,配置内容参照下方
#再运行容器
docker run -d -p 80:80 --name nginx -v /opt/nginx/html:/usr/share/nginx/html -v /opt/nginx/conf/nginx.conf:/etc/nginx/nginx.conf -v
/opt/nginx/logs:/var/log/nginx nginx
nginx.conf 配置如下:

在浏览器中测试:172.16.116.100nginx服务器地址

经过测试,通过nginx访问服务一切正常。

压力测试
注意:先把数据库库存量还原到 5000
参照之前的测试用例,再创建一个新的测试组:参数给之前一样

配置nginx的地址及 服务的访问路径如下:

 测试结果:性能只是略有提升。

数据库库存剩余量如下:

又出现了并发问题,即出现了超卖现象。
mysql 锁演示
除了使用 jvm 锁之外,还可以使用数据锁: 悲观锁 或者 乐观锁
悲观锁:在读取数据时锁住那几行,其他对这几行的更新需要等到悲观锁结束时才能继续 。
乐观所:读取数据时不锁,更新时检查是否数据已经被更新过,如果是则取消当前更新,一般在悲观锁的等待时间过长而不能接受时我们才会选择乐观锁。
悲观锁

在select的时候就会加锁,采用先加锁后处理的模式,虽然保证了数据处理的安全性,但也会阻塞其他线程的写操作。悲观锁适用于写多读少的场景,因为拿不到锁的线程,会将线程挂起,交出CPU资源,可以把CPU给其他线程使用,提高了CPU的利用率。
乐观锁

在select的时候不会加锁,是基于程序实现的,所以不会存在死锁的情况。
适用于读多写少的场景(写的并发量相对不高),可以提高系统的吞吐量。
因为如果写多的话,乐观锁会有很大机率更新失败,需要不断的自旋执行查找和更新操作。
自旋的时候会一直占用CPU,会耗费大量的CPU资源。
锁分类:

    悲观锁:具有强烈的独占和排他特性,在整个数据处理过程中,将数据处于锁定状态。适合于写比较多,会阻塞读操作。

    乐观锁:采取了更加宽松的加锁机制,大多是基于数据版本( Version )及时间戳来实现。。适合于读比较多,不会阻塞读。
    
    独占锁、互斥锁、排他锁:保证在任一时刻,只能被一个线程独占排他持有。synchronized、ReentrantLock。

    共享锁:可同时被多个线程共享持有。CountDownLatch到计数器、Semaphore信号量。
    
    可重入锁:又名递归锁。同一个线程在外层方法获取锁的时候,在进入内层方法时会自动获取锁。

    不可重入锁:不可重入锁不可递归调用,递归调用就发生死锁,不可重入锁也叫自旋锁。
    
    公平锁:有优先级的锁,先来先得,谁先申请锁就先获取到锁。
    非公平锁:无优先级的锁,后来者也有机会先获取到锁。
    
    自旋锁:当线程尝试获取锁失败时(锁已经被其它线程占用了),无限循环重试尝试获取锁。

    阻塞锁:当线程尝试获取锁失败时,线程进入阻塞状态,直到接收信号后被唤醒。在竞争激烈情况下,性能较高。
    
    读锁:共享锁。

    写锁:独占排他锁。
    
    偏向锁:一直被一个线程所访问,那么该线程会自动获取锁。

    轻量级锁(CAS):当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。

    重量级锁:当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候(10次),还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让他申请的线程进入阻塞,性能降低。

    以上其实是synchronized的锁升级过程。
    
    表级锁:对整张表加锁,加锁快开销小,不会出现死锁,但并发度低,会增加锁冲突的概率。

    行级锁:是mysql粒度最小的锁,只针对操作行,可大大减少锁冲突概率,并发度高,但加锁慢,开销大,会出现死锁。
具体锁实现

    jvm:
        ReentrantLock悲观的独占的可重入的可公平可不公平锁
        synchronized悲观的独占的可重入的非公平锁
        无锁 --> 偏向锁(同一个线程再次获取锁) --> 轻量级锁(自旋) --> 重量级锁
        ReentrantLock + synchronized(必要条件)
        1.单个jvm实例 单机
        2.必须单例
        3.与事务并存问题(上面说到的事务问题)
        总之,不适合于保证数据库数据可靠性
    
    mysql:
        select ... for update:悲观的独占的
        select ... lock in share mode
        1.直接更新时判断。在更新中判断库存是否大于0 update table set surplus = (surplus - buyQuantity) where id = 1 and (surplus - buyQuantity) > 0 ;
        解决jvm锁多例模式锁失效问题及事务共存问题
        锁范围控制:条件字段必须创建索引;查询条件必须具体的值
        同一个商品有多个库存时,无法解决。
        无法记录库存变化前后的状态
        2.悲观锁:select ... for update
        库存操作要统一:不能有的操作是select ... for update 而有的操作是普通的select
        死锁风险:多条记录时,加锁顺序要一致
        阻塞及性能问题
        3.乐观锁:version 或者 时间戳(CAS思想)
        ABA问题
        失败需要重试,高并发情况下性能不高
        读写分离情况下导致乐观锁不可靠
 
mysql 悲观锁
MySQL InnoDB 中,预设的 Tansaction isolation level REPEATABLE READ (可重读)
SELECT 的读取锁定主要分为两种方式:
SELECT ... LOCK IN SHARE MODE(共享锁)
SELECT ... FOR UPDATE (悲观锁)
这两种方式在事务 (Transaction) 进行当中 SELECT 到同一个数据表时,都必须等待其它事务数据被提交(Commit) 后才会执行。 而主要的不同在于 LOCK IN SHARE MODE 在有一方事务要 Update 同一个表单时很容易造成死锁。
简单的说,如果 SELECT 后面若要 UPDATE 同一个表单,最好使用 SELECT ... FOR UPDATE
代码实现
改造 StockService
StockeMapper 中定义 selectStockForUpdate 方法:
public interface StockMapper extends BaseMapper<Stock> {

    public Stock selectStockForUpdate(Long id);

}
StockMapper.xml 中定义对应的配置:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.lzx.distributedlock.mapper.StockMapper">

   <select id="selectStockForUpdate" resultType="com.lzx.distributedlock.pojo.Stock">
       select * from db_stock where id = #{id} for update
   </select>

</mapper>
压力测试
注意:测试之前,需要把库存量改成 5000 。压测数据如下:比 jvm 性能高很多,比无锁要低将近 1
mysql 数据库存:
mysql 乐观锁
乐观锁( Optimistic Locking ) 相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则重试。那么 如何实现乐观锁呢?
使用数据版本(Version )记录机制实现,这是乐观锁最常用的实现 方式。一般是通过为数据库表增加 一个数字类型的 “version” 字段来实现。当读取数据时,将 version 字段的值一同读出,数据每更新一 次,对此 version 值加一。当我们提交更新的时候,判断数据库表对应记录 的当前版本信息与第一次取 出来的 version 值进行比对,如果数据库表当前版本号与第一次取出来的 version 值相等,则予以更新。
db_stock 表添加 version 字段:

对应也需要给Stock实体类添加version属性。

代码实现

重启后使用jmeter压力测试工具结果如下:

修改测试参数如下:
测试结果如下:
说明乐观锁在并发量越大的情况下,性能越低(因为需要大量的重试);并发量越小,性能越高。
mysql 锁缺陷
在数据库集群情况下会导致数据库锁失效,并且很多数据库集群的中间件压根就不支持悲观锁。例如: mycat 在读写分离的场景下可能会导致乐观锁不可靠。
这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。
这就需要引入分布式锁来解决问题。

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

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

相关文章

2023年京东618预售数据:传统滋补成预售黑马,预售额超27亿

这一期主要分享一下此次京东618预售期间的一个黑马行业——传统滋补。不管是从预售量和预售额来看&#xff0c;传统滋补品类的成绩都是此次大促中的佼佼者。 究其原因&#xff0c;近几年养生滋补也掀起了一股“国潮风”。在小红书、抖音等社交平台上&#xff0c;关于“健康养生…

车载摄像头专用——拓尔微低功耗超高集成PMIC TMI7205B

“2023将是汽车行业的大变革之年&#xff0c;全球迎来L2向L3/L4跨越窗口。”越高级别的自驾对周围环境感知要求越高&#xff0c;车载摄像头“高清化”势不可挡&#xff0c;目前已从传统的100万直接跃升至800万像素摄像头&#xff0c;甚至在供应层面&#xff0c;已有超1500万高像…

Vue.js 中的 $forceUpdate 方法是什么?有什么作用?

Vue.js 中的 $forceUpdate 方法是什么&#xff1f;有什么作用&#xff1f; 在 Vue.js 中&#xff0c;$forceUpdate 方法是一个很常见的方法之一。它可以强制组件重新渲染&#xff0c;从而让组件的视图更新。本文将介绍 $forceUpdate 方法的使用方法和作用&#xff0c;并给出一…

代码随想录算法训练营第二天| 977.有序数组的平方 209.长度最小的子数组 59.螺旋矩阵||

LeetCode977.有序数组的平方 链接&#xff1a;有序数组的平方 给你一个按 非递减顺序 排序的整数数组 nums&#xff0c;返回 每个数字的平方 组成的新数组&#xff0c;要求也按 非递减顺序 排序。 看到这道题&#xff0c;我第一反应就是把每个数的平方算出来然后排序&#xff…

结构体大小的计算

结构体计算要遵循字节对齐原则。 结构体默认的字节对齐一般满足三个准则&#xff1a; 结构体变量的首地址能够被其最宽基本类型成员的大小所整除&#xff1b;结构体每个成员相对于结构体首地址的偏移量&#xff08;offset&#xff09;都是成员大小的整数倍&#xff0c;如有需…

【并发篇】04 线程池核心参数

这道题其实就是在问java中线程池的实现类ThreadPoolExecutor&#xff0c;这个类参数最多的构造方法有7个参数。 线程池本质上就是管理一组线程&#xff0c;用来执行提交给线程池的任务。提交任务用的是submit(task)。 corePoolSize设置核心线程数。核心线程执行完任务后仍然需…

java培训机构学校教学教务选课管理平台springboot+vue

近年来&#xff0c;随着培训机构机构规模的逐渐增大&#xff0c;人工书写的方式已经不能满足如此庞大的数据。为了更好的适应信息时代的高效性&#xff0c;一个利用计算机来实现培训机构教务管理工作的系统将必然诞生。基于这一点&#xff0c;设计了一个培训机构教务管理系统&a…

视觉相机模型以及投影原理推导——(单目)

相机模型简介 参考文献&#xff1a;视觉SLAM十四讲、视觉惯性SLAM理论与源码分析、该博客、文中的公式直接引用上面的文章&#xff0c;如有侵权请联系本人删除 1、针孔相机模型 投影过程 三维世界中的物体&#xff08;目标点&#xff09;P反射光线&#xff0c;通过相机光心&am…

RL - 强化学习 蒙特卡洛 (Monte-Carlo) 方法计算状态价值

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://blog.csdn.net/caroline_wendy/article/details/131102145 在强化学习中&#xff0c;状态价值 (State Value) 是指在特定状态下&#xff0c;智能体能够从该状态开始执行一系列动作&…

你还在用U盘和聊天工具来处理文档吗?ONLYOFFICE的协作空间来解决你的痛点了!

你还在用U盘和聊天工具来处理文档吗&#xff1f;ONLYOFFICE的协作空间来解决你的痛点了&#xff01; 说起Office办公软件&#xff0c;大家想到的首先就是Word、PPT、Excel&#xff0c;这是微软Office的三件套&#xff0c;从我们当代人念大学写论文时候开始学着用&#xff0c;到…

PDF或图片文档内容识别、关系抽取

需求 自动识别法院和公积金中心的文书&#xff08;调解书、判决书、裁定书、通知书&#xff09;扫描件&#xff08;PDF或图片&#xff09;&#xff0c;获取特定结构的数据&#xff0c;自动对比。抽取结构如&#xff1a; [标题,诉讼案号,执行案号,公积金,{原告: [姓名, 单位, 生…

无代码让我彻夜难眠

最近人工智能非常的火&#xff0c;特别是GPT&#xff0c;让一些程序员很慌。 但是GPT终归还是一个智能的搜索引擎&#xff0c;你可以问它某个问题或者算法问题&#xff0c;你让它快速开发一个系统&#xff0c;它能吗&#xff1f; 不能&#xff01; 但是无代码可以&#xff0…

【Android Studio】Flamingo版本 更新gradle插件 7.+到8.+

步骤 build.gradle(module) android {namespace //adddefaultConfig {applicationId }}AndroidManifest.xml 取消package属性 <?xml version"1.0" encoding"utf-8"?> <manifest xmlns:android"http://schemas.android.com/apk/res/andr…

nginx负载均衡+反向代理

最近业务上遇到一个需求&#xff0c;其它系统因业务校验需要调用上级系统进行数据发送或校验&#xff0c;如果上级系统停机维护&#xff0c;其它下级系统发送的http通讯会丢失&#xff0c;还要一次次补发数据&#xff0c;耗费人工与时间。使用nginx反向代理解决了部分需求。 目…

【Java项目】从0到1完成Nacos配置文件扩展和共享

文章目录 环境配置配置文件名称空间和组扩展配置共享配置文件配置文件优先级 使用nacos作用配置中心的好处在于我们可以在云端上修改配置文件之后&#xff0c;使得本地的配置重新生效&#xff0c;从而做到不用重启项目也可以加载新的配置。 环境配置 首先引入依赖&#xff0c;…

【基础知识整理】图的基本概念 邻接矩阵 邻接表

一、图概述 定义&#xff1a; 图(graph)是由一些点(vertex)和这些点之间的连线(edge)所组成的&#xff1b; 其中&#xff0c;点通常被成为"顶点(vertex)“&#xff0c;而点与点之间的连线则被成为"边或弧”(edege)。 通常记为&#xff0c;G(V,E)。 图是一种重要的…

ANR中为什么会出现堆栈漂移

前言 我们排查ANR问题的时候&#xff0c;会发现有时候anr文件中捕获的堆栈信息&#xff0c;并不准确&#xff0c;而且经常会打印下面这样的堆栈&#xff1a; "main" prio5 tid1 Native| group"main" sCount1 ucsCount0 flags1 obj0x71ac6f78 self0xb4000…

java+mysql校园外卖订餐管理系统servlet

3.1 课题目的 对于客户而言&#xff0c;外卖订单管理系统最好是越方便操作&#xff0c;越简单越好。客户对于外卖订单系统&#xff0c;对他的首要要求就是能够安全&#xff0c;顺利的完成订单。但是很多时候开发人员更在意一些创新的模式&#xff0c;去增加用户的体验感或增值服…

关于变邻域搜索求解柔性作业车间问题的探讨

邻域结构交换内部关键块的操作 譬如&#xff0c;某案例的内部关键块为501—601—502—701&#xff0c;部分OS加工顺序码如下(标注的黄色底纹&#xff1a;为内部关键块) 在移动内部关键快操作时&#xff0c;请教各位是否是如下的变换&#xff1a; ① 块尾701 移至 块内工序50…

vs Git 本地服务器仓库配置

1、安装Git程序 Git 下载地址一https://git-scm.com/download/winGit 下载地址二We bring the awesome Git VCS to Windowshttps://gitforwindows.org/ 2、新建本地服务端仓库 2.1、新建文件夹&#xff0c;并创建仓库 在本地新建文件夹&#xff1a;//本地服务器仓库 在服务…