分布式锁之传统锁回顾(一)

1. 传统锁回顾

1.1. 从减库存聊起

多线程并发安全问题最典型的代表就是超卖现象

库存在并发量较大情况下很容易发生超卖现象,一旦发生超卖现象,就会出现多成交了订单而发不了货的情况。

场景:

商品S库存余量为5时,用户A和B同时来购买一个商品,此时查询库存数都为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才对

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;

表中数据如下:

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 https://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.2.11.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.atguigu</groupId><artifactId>distributed-lock</artifactId><version>0.0.1-SNAPSHOT</version><name>distributed-lock</name><description>分布式锁demo工程</description><properties><java.version>1.8</java.version></properties><dependencies><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.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.Driverurl: jdbc:mysql://172.16.116.100:3306/testusername: rootpassword: rootredis:host: 172.16.116.100

DistributedLockApplication启动类:

@SpringBootApplication
@MapperScan("com.atguigu.distributedlock.mapper")
public class DistributedLockApplication {public static void main(String[] args) {SpringApplication.run(DistributedLockApplication.class, args);}}

Stock实体类:

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

StockMapper接口:

public interface StockMapper extends BaseMapper<Stock> {
}

1.3. 简单实现减库存

接下来咱们代码实操一下。

StockController:

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

StockService:

@Service
public class StockService {@Autowiredprivate 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,没有任何问题。

1.4. 演示超卖现象

接下来咱们使用jmeter压力测试工具,高并发下压测一下,添加线程组:并发100循环50次,即5000次请求。

给线程组添加HTTP Request请求:

填写测试接口路径如下:

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

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

  • 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次,平均请求时间37ms,中位数(50%)请求是在36ms内完成的,错误率0%,每秒钟平均吞吐量2568.1次。

查看mysql数据库剩余库存数:还有4870

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

1.5. jvm锁问题演示

1.5.1. 添加jvm锁

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

重启tomcat服务,再次使用jmeter压力测试,效果如下:

查看mysql数据库:

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

1.5.2. 原理

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

1.6. 多服务问题

使用jvm锁在单工程单服务情况下确实没有问题,但是在集群情况下会怎样?

接下启动多个服务并使用nginx负载均衡,结构如下:

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

1.6.1. 安装配置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配置如下:

user  nginx;
worker_processes  1;error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;events {worker_connections  1024;
}http {include       /etc/nginx/mime.types;default_type  application/octet-stream;log_format  main  '$remote_addr - $remote_user [$time_local] "$request" ''$status $body_bytes_sent "$http_referer" ''"$http_user_agent" "$http_x_forwarded_for"';access_log  /var/log/nginx/access.log  main;sendfile        on;#tcp_nopush     on;keepalive_timeout  65;#gzip  on;#include /etc/nginx/conf.d/*.conf;upstream distributed {server 172.16.116.1:8000;server 172.16.116.1:8100;server 172.16.116.1:8200;}server {listen       80;server_name  172.16.116.100;location / {proxy_pass http://distributed;}}}

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

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

1.6.2. Jmeter压力测试

注意:先把数据库库存量还原到5000。

参照之前的测试用例,再创建一个新的测试组:参数给之前一样

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

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

数据库库存剩余量如下:

又出现了并发问题,即出现了超卖现象。

1.7. mysql锁演示

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

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

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

  2. 悲观锁:在读取数据时锁住那几行,其他对这几行的更新需要等到悲观锁结束时才能继续 。

    select ... for update

  3. 乐观锁:读取数据时不锁,更新时检查是否数据已经被更新过,如果是则取消当前更新进行重试。

    version 或者 时间戳(CAS思想)。

1.7.1. 一个sql

略。。

1.7.2. 悲观锁

在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.atguigu.distributedlock.mapper.StockMapper"><select id="selectStockForUpdate" resultType="com.atguigu.distributedlock.pojo.Stock">select * from db_stock where id = #{id} for update</select>
</mapper>

压力测试

注意:测试之前,需要把库存量改成5000。压测数据如下:比jvm性能高很多,比无锁要低将近1倍

mysql数据库存:

1.7.3. 乐观锁

乐观锁( Optimistic Locking ) 相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则重试。那么我们如何实现乐观锁呢

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

给db_stock表添加version字段:

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

代码实现

public void checkAndLock() {// 先查询库存是否充足Stock stock = this.stockMapper.selectById(1L);// 再减库存if (stock != null && stock.getCount() > 0){// 获取版本号Long version = stock.getVersion();stock.setCount(stock.getCount() - 1);// 每次更新 版本号 + 1stock.setVersion(stock.getVersion() + 1);// 更新之前先判断是否是之前查询的那个版本,如果不是重试if (this.stockMapper.update(stock, new UpdateWrapper<Stock>().eq("id", stock.getId()).eq("version", version)) == 0) {checkAndLock();}}
}

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

修改测试参数如下:

测试结果如下:

说明乐观锁在并发量越大的情况下,性能越低(因为需要大量的重试);并发量越小,性能越高。

1.7.4. mysql锁总结

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

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

优先选择:一个sql

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

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

优先选择:mysql悲观锁

不推荐jvm本地锁。

1.8. redis乐观锁

利用redis监听 + 事务

watch stock
multi
set stock 5000
exec

如果执行过程中stock的值没有被其他链接改变,则执行成功

如果执行过程中stock的值被改变,则执行失败效果如下:

具体代码实现,只需要改造对应的service方法:

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;}});
}

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

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

相关文章

PC8223(CC/CV控制)高耐压输入5V/3.4A同步降压电路内建补偿带恒流恒压输出

概述 PC8233&#xff08;替代CX8853&#xff09;是一款同步降压调节器,输出电流高达3.4A,操作范围从8V到32V的宽电源电压。内部补偿要求最低数量现成的标准外部组件。PC8233在CC&#xff08;恒定输出电流&#xff09;模式或CV&#xff08;恒定输出电压&#xff09;模式&#x…

代码随想录算法训练营Day 60 || 84.柱状图中最大的矩形

84.柱状图中最大的矩形 力扣题目链接(opens new window) 给定 n 个非负整数&#xff0c;用来表示柱状图中各个柱子的高度。每个柱子彼此相邻&#xff0c;且宽度为 1 。 求在该柱状图中&#xff0c;能够勾勒出来的矩形的最大面积。 1 < heights.length <10^50 < hei…

CVE-2022-0543(Redis 沙盒逃逸漏洞)

简介 CVE-2022-0543是一个与Redis相关的安全漏洞。在Redis中&#xff0c;用户连接后可以通过eval命令执行Lua脚本&#xff0c;但在沙箱环境中脚本无法执行命令或读取文件。然而&#xff0c;攻击者可以利用Lua沙箱中遗留的变量package的loadlib函数来加载动态链接库liblua5.1.s…

VirtualBox下win主机如何访问linux虚拟机文件夹

目录 ​编辑 方法1&#xff1a;通过VirtualBox自带的共享文件夹&#xff08;Win->linux&#xff09; 方法2&#xff1a;通过Samba方法本地网络访问(Linux->win) 我使用的VirtualBox版本为7.0.4,主机是Window系统&#xff0c;虚拟机是Linux系统 方法1&#xff1a;通过Vir…

【SpringBoot篇】Spring_Task定时任务框架

文章目录 &#x1f339;概述&#x1f33a;应用场景&#x1f384;cron表达式&#x1f6f8;入门案例&#x1f38d;实际应用 &#x1f339;概述 Spring Task 是 Spring 框架提供的一种任务调度和异步处理的解决方案。可以按照约定的时间自动执行某个代码逻辑它可以帮助开发者在 S…

【深度学习】学习率及多种选择策略

学习率是最影响性能的超参数之一&#xff0c;如果我们只能调整一个超参数&#xff0c;那么最好的选择就是它。相比于其它超参数学习率以一种更加复杂的方式控制着模型的有效容量&#xff0c;当学习率最优时&#xff0c;模型的有效容量最大。本文从手动选择学习率到使用预热机制…

RocketMQ-NameServer详解

前言 ​ RocketMQ架构上主要分为四部分, Broker、Producer、Consumer、NameServer&#xff0c;其他三个都会与NameServer进行通信。 Producer: ​ **消息发布的角色&#xff0c;可集群部署。**通过NameServer集群获得Topic的路由信息&#xff0c;包括Topic下面有哪些Queue&a…

数据结构与算法编程题15

设计一个算法&#xff0c;通过遍历一趟&#xff0c;将链表中所有结点的链接方向逆转&#xff0c;仍利用原表的存储空间。 #include <iostream> using namespace std;typedef int Elemtype; #define ERROR 0; #define OK 1;typedef struct LNode {Elemtype data; …

【从入门到起飞】JavaSE—多线程(3)(生命周期,线程安全问题,同步方法)

&#x1f38a;专栏【JavaSE】 &#x1f354;喜欢的诗句&#xff1a;路漫漫其修远兮&#xff0c;吾将上下而求索。 &#x1f386;音乐分享【如愿】 &#x1f384;欢迎并且感谢大家指出小吉的问题&#x1f970; 文章目录 &#x1f354;生命周期&#x1f384;线程的安全问题&#…

YOLOv7独家改进: Inner-IoU基于辅助边框的IoU损失,高效结合 GIoU, DIoU, CIoU,SIoU 等 | 2023.11

💡💡💡本文独家改进:Inner-IoU引入尺度因子 ratio 控制辅助边框的尺度大小用于计算损失,并与现有的基于 IoU ( GIoU, DIoU, CIoU,SIoU )损失进行有效结合 推荐指数:5颗星 新颖指数:5颗星 收录: YOLOv7高阶自研专栏介绍: http://t.csdnimg.cn/tYI0c …

大模型AI Agent 前沿调研

前言 大模型技术百花齐放&#xff0c;越来越多&#xff0c;同时大模型的落地也在紧锣密鼓的进行着&#xff0c;其中Agent智能体这个概念可谓是火的一滩糊涂。 今天就分享一些Agent相关的前沿研究&#xff08;仅限基于大模型的AI Agent研究&#xff09;&#xff0c;包括一些论…

完美解决AttributeError: module ‘numpy‘ has no attribute ‘typeDict‘

文章目录 前言一、完美解决办法安装低版本1.21或者1.19.3都可以总结 前言 这个问题从表面看就是和numpy库相关&#xff0c;所以是小问题&#xff0c;经过来回调试安装numpy&#xff0c;发现是因为目前的版本太高&#xff0c;因此我们直接安装低版本numpy。也不用专门卸载目前的…

Qt全球峰会2023中国站 参会概要

Qt全球峰会2023中国站 参会概要 前言峰会议程签到 & Demo 演示开场致辞Qt Group 产品总监演讲&#xff08;产品开发的趋势-开放的软件、工具和框架&#xff09;产品战略QtQuick or QtWidgets&#xff08;c or qml&#xff09;Qt如何定义AI个人看法 Qt 在券商数字化转型和信…

【MySQL】内连接和外连接

内连接和外连接 前言正式开始内连接外连接左外连接右外连接 前言 前一篇讲多表查询的时候讲过笛卡尔积&#xff0c;其实笛卡尔积就算一种连接&#xff0c;不过前一篇讲的时候并没有细说连接相关的内容&#xff0c;本篇就来详细说说表的连接有哪些。 本篇博客中主要用到的还是…

中部A股第一城,长沙如何赢商?

文|智能相对论 作者|范柔丝 长沙的马路&#xff0c;都很有故事。 一条解放西路&#xff0c;是全国人民都争相打卡的娱乐地标&#xff1b;一条太平街&#xff0c;既承载了历史的厚重又演绎着现代的鲜活...... 但如果来到河西的桐梓坡路&#xff0c;风景会变得截然不同。 沿…

安装MySQL搭建论坛

课前默写&#xff1a; 1、nginx配置文件的区域有哪些 ①全局区域 ②events区域 ③http区域 2、区域模块的作用 全局区域模块主要是用户和工作进程 events区域模块配置最大连接数时需先配置:vim /etc/limits.conf 因为系统默认最大是1024 http区域模块&#xff1a;代理地…

BUUCTF [HBNIS2018]excel破解 1

BUUCTF:https://buuoj.cn/challenges 题目描述&#xff1a; 得到的 flag 请包上 flag{} 提交。来源&#xff1a; https://github.com/hebtuerror404/CTF_competition_warehouse_2018 密文&#xff1a; 下载附件&#xff0c;得到一个attachment.xls文件。 解题思路&#xff…

计算机视觉的应用19-基于pytorch框架搭建卷积神经网络CNN的卫星地图分类问题实战应用

大家好&#xff0c;我是微学AI&#xff0c;今天给大家介绍一下计算机视觉的应用19-基于pytorch框架搭建卷积神经网络CNN的卫星地图分类问题实战应用。随着遥感技术和卫星图像获取能力的快速发展&#xff0c;卫星图像分类任务成为了计算机视觉研究中一个重要的挑战。为了促进这一…

git的用法

目录 一、为什么需要git 二、git基本操作 2.1、初始化git仓库 2.2、配置本地仓库的name和email 2.3、认识工作区、暂存区、版本库 三、git的实际操作 3.1 提交文件 3.2 查看git状态以及具体的修改 3.3 git版本回退 git reset 3.1 撤销修改 四、git分支管理 4.…

vue el-table (固定列+滚动列)【横向滚动条】确定滚动条是在列头还是列尾

效果图&#xff1a; 代码实现&#xff1a; html&#xff1a; <script src"//unpkg.com/vue2/dist/vue.js"></script> <script src"//unpkg.com/element-ui2.15.14/lib/index.js"></script> <div id"app" style&quo…