​【五一创作】基于mysql关系型实现分布式锁

news/2024/2/28 17:46:39

看完该文预计用时:15分钟

看之前应具体的技术栈:springboot mysql nginx(了解即可)

目录

0.写在前面

1. 从减库存聊起

1.1. 环境准备

  1.2. 简单实现减库存

 1.3. 演示超卖现象

1.4. jvm锁问题演示 

1.4.2. 原理

1.5. 多服务问题 

1.5.1. 安装配置nginx

1.5.2. 压力测试

 1.6. mysql锁演示

1.6.1. mysql悲观锁

1.6.2. mysql乐观锁 

 1.6.3. mysql锁缺陷

 2. 基于mysql实现分布式锁

2.1. 基本思路 

2.2. 代码实现

2.3. 缺陷及解决方案 


0.写在前面

在多线程高并发场景下,为了保证资源的线程安全问题,jdk为我们提供了synchronized关键字和
ReentrantLock可重入锁,但是它们只能保证一个jvm内的线程安全。在分布式集群、微服务、云原生横行的当下,如何保证不同进程、不同服务、不同机器的线程安全问题,jdk并没有给我们提供既有的解决方案。此时,我们就必须借助于相关技术手动实现了。目前主流的实现有三种方式:
1. 基于mysql关系型实现
2. 基于redis非关系型数据实现
3. 基于zookeeper实现

这篇文章主要讲解的是基于基于mysql关系型实现分布式锁

1. 从减库存聊起

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

场景:
        商品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才对

1.1. 环境准备

为了模拟具体场景我们需要准备开发环境

首先需要在mysql数据库中准备一张表:

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;

 表中数据如下:

 创建分布式锁demo工程:

 建立以下工具目录结构:

 pom依赖文件:

<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: root

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.2. 简单实现减库存

接下来咱们代码实操一下

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.3. 演示超卖现象

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

 

 给线程组添加HTTP Request请求:

填写测试接口路径如下:

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

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

测试结果:请求总数5000次,平均请求时间202ms,中位数(50%)请求是在173ms内完成的,90%请求是在344ms内完成的,最小耗时12ms,最大耗时1125ms,错误率0%,每秒钟平均473.8次。

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

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

1.4. jvm锁问题演示 

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

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

查看mysql数据库:

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

1.4.2. 原理

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

1.5. 多服务问题 

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

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

1.5.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
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.5.2. 压力测试

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

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

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

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

 数据库库存剩余量如下:

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

 1.6. mysql锁演示

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

悲观锁:在读取数据时锁住那几行,其他对这几行的更新需要等到悲观锁结束时才能继续 。 乐观所:读取数据时不锁,更新时检查是否数据已经被更新过,如果是则取消当前更新,一般在悲观锁 的等待时间过长而不能接受时我们才会选择乐观锁。

1.6.1. 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.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.6.2. mysql乐观锁 

乐观锁( 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.6.3. mysql锁缺陷

在数据库集群情况下会导致数据库锁失效,并且很多数据库集群的中间件压根就不支持悲观锁。例如:mycat,在读写分离的场景下可能会导致乐观锁不可靠。 这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。

 2. 基于mysql实现分布式锁

 不管是jvm锁还是mysql锁,为了保证线程的并发安全,都提供了悲观独占排他锁。所以独占排他也是 分布式锁的基本要求。 可以利用唯一键索引不能重复插入的特点实现。设计表如下:

CREATE TABLE `db_lock` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`lock_name` varchar(50) NOT NULL COMMENT '锁名',`class_name` varchar(100) DEFAULT NULL COMMENT '类名',`method_name` varchar(50) DEFAULT NULL COMMENT '方法名',`server_name` varchar(50) DEFAULT NULL COMMENT '服务器ip',`thread_name` varchar(50) DEFAULT NULL COMMENT '线程名',`create_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '获取锁时间',`desc` varchar(100) DEFAULT NULL COMMENT '描述',PRIMARY KEY (`id`),UNIQUE KEY `idx_unique` (`lock_name`)
) ENGINE=InnoDB AUTO_INCREMENT=1332899824461455363 DEFAULT CHARSET=utf8;

Lock实体类:  

@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("db_lock")public class Lock {private Long id;private String lockName;private String className;private String methodName;private String serverName;private String threadName;private Date createTime;private String desc;
}

LockMapper接口:

public interface LockMapper extends BaseMapper<Lock> {
}

2.1. 基本思路 

synchronized关键字和ReetrantLock锁都是独占排他锁,即多个线程争抢一个资源时,同一时刻只有 一个线程可以抢占该资源,其他线程只能阻塞等待,直到占有资源的线程释放该资源。

  1. 线程同时获取锁(insert)
  2. 获取成功,执行业务逻辑,执行完成释放锁(delete)
  3. 其他线程等待重试

2.2. 代码实现

改造StockService:

@Servicepublic class StockService {@Autowiredprivate StockMapper stockMapper;@Autowiredprivate LockMapper lockMapper;/*** 数据库分布式锁*/public void checkAndLock() {// 加锁Lock lock = new Lock(null, "lock", this.getClass().getName(), new 
Date(), null);try {this.lockMapper.insert(lock);} catch (Exception ex) {// 获取锁失败,则重试try {Thread.sleep(50);this.checkAndLock();} catch (InterruptedException e) {e.printStackTrace();}}// 先查询库存是否充足Stock stock = this.stockMapper.selectById(1L);// 再减库存if (stock != null && stock.getCount() > 0){stock.setCount(stock.getCount() - 1);this.stockMapper.updateById(stock);}// 释放锁this.lockMapper.deleteById(lock.getId());}
}

加锁:  

// 加锁
Lock lock = new Lock(null, "lock", this.getClass().getName(), new Date(), null);
try {this.lockMapper.insert(lock);
} catch (Exception ex) {// 获取锁失败,则重试try {Thread.sleep(50);this.checkAndLock();} catch (InterruptedException e) {e.printStackTrace();}
}

解锁:

// 释放锁
this.lockMapper.deleteById(lock.getId());

使用Jmeter压力测试结果:

 可以看到性能感人。mysql数据库库存余量为0,可以保证线程安全。 

2.3. 缺陷及解决方案 

1. 这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。

解决方案:给锁数据库 搭建主备

2. 这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。

解决方案:只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。

3. 这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。

解决方案:记录获取锁的主机信息和线程信息,如果相同线程要获取锁,直接重入。

4. 受制于数据库性能,并发能力有限。

解决方案:无法解决。


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

相关文章

【51单片机】数码管显示(样例展示以及异常分析)

🎊专栏【51单片机】 🍔喜欢的诗句:更喜岷山千里雪 三军过后尽开颜。 🎆音乐分享【如愿】 大一同学小吉,欢迎并且感谢大家指出我的问题🥰 ⭐数码管 比如要显示“6”,那么下面图片中,AFEDCG=1,B=0 对应到数码管上,就是 ⭐原理 🎊P22~P24控制LED1~

transformer and DETR

RNN 很难并行化处理 Transformer 1、Input向量x1-x4分别乘上矩阵W得到embedding向量a1-a4。 2、向量a1-a4分别乘上Wq、Wk、Wv得到不同的qi、ki、vi&#xff08;i{1,2,3,4}&#xff09;。 3、使用q1对每个k&#xff08;ki&#xff09;做attention得到a1,i&#xff08;i{1,2,3,4…

FPGA时序约束(六)时序例外约束

系列文章目录 FPGA时序约束&#xff08;一&#xff09;基本概念入门及简单语法 FPGA时序约束&#xff08;二&#xff09;利用Quartus18对Altera进行时序约束 FPGA时序约束&#xff08;三&#xff09;时序约束基本路径的深入分析 FPGA时序约束&#xff08;四&#xff09;主时…

了解标量、向量和点积

数据科学基础数学&#xff1a;线性代数简介 了解标量、向量和点积 机器只能按着算法理解和处理数据结构存储的数字. 例如创建垃圾邮件检测器&#xff0c;则首先必须将文本数据转换为数字(通过单词嵌入)。 两个句子之间的余弦相似性 两个句子之间的余弦相似性可以通过它们的向量…

2023前端大厂高频面试题之项目篇

系列文章&#xff1a; 2023前端大厂面试题之JavaScript篇(1) 2023前端大厂面试题之JavaScript篇(2) 2023前端大厂面试题之JavaScript篇(3) 2023前端大厂面试题之JavaScript篇(4) 2023前端大厂高频面试题之CSS篇 2023前端大厂高频面试题之Vue篇(1) 2023前端大厂高频面试题之Vue篇…

【数据库复习】第四章数据库保护 1

数据库安全性&#xff1a; 数据库的一大特点是数据可以共享 数据共享必然带来数据库的安全性问题 数据库系统中的数据共享不能是无条件的共享 用户标识与鉴别 用户名和口令易被窃取&#xff0c;每个用户预先约定好一个计算过程或者函数 存取控制 常用存取控制方法 自主存…

第13章 项目合同管理

文章目录 13.2.1 按信息系统 范围 划分的合同分类 4451、总承包合同2、单项工程承包合同3、分包合同 13.2.2 按项目 付款方式 划分的合同分类 4461、总价合同2、成本补偿合同&#xff08;卖方有利&#xff09;3、工料合同 13.3.1 项目合同的内容 44713.3.2 项目合同签订的注意事…

【Java】类和对象,封装

目录 1.类和对象的定义 2.关键字new 3.this引用 4.对象的构造及初始化 5.封装 //包的概念 //如何访问 6.static成员 7.代码块 8.对象的打印 1.类和对象的定义 对象&#xff1a;Java中一切皆对象。 类&#xff1a;一般情况下一个Java文件一个类&#xff0c;每一个类…

时间序列分析-1

时间序列 是指将数值按其发生的时间先后顺序排列而成的数列。时间序列分析的主要目的是根据已有的历史数据对未来进行预测。经济数据中大多数以时间序列的形式给出。根据观察时间的不同&#xff0c;时间序列中的时间可以是年份、季度、月份或其他任何时间形式。如某股票的每天…

SimpleDateFormat以及Date的使用

Date Date currentTime new Date(); 获取当前的时间 输出&#xff1a; System.out.println(currentTime); 就会以这样的格式输出 那我们不想要这样的格式&#xff0c;而是输出格式怎么办呢&#xff1f; SimpleDateFormat闪亮登场 食用方法&#xff1a; SimpleDateForma…

SpringCloud实现电影微服务,也注册到 EurekaServer 中,通过 Http 协议访问已注册到生态圈中的用户微服务

SpringCloud&#xff08;第 005 篇&#xff09;电影微服务&#xff0c;也注册到 EurekaServer 中&#xff0c;通过 Http 协议访问已注册到生态圈中的用户微服务 一、大致介绍 1、在 eureka 服务治理框架中&#xff0c;微服务与微服务之间通过 Http 协议进行通信&#xff1b; 2…

hitcontraining_uaf

1&#xff0c;三连 基本信息&#xff1a;x86-32-el,堆题思路&#xff1b; 保护&#xff1a;Partial RELRO。 堆题多看一个Libc&#xff1a; 2,IDA分析 main功能&#xff1a; add_note()功能&#xff1a; malloc了两次&#xff1a; 8字节填充&#xff08;利用点之一&#xf…

学习笔记:分支结构

✨博文作者&#xff1a;烟雨孤舟 &#x1f496; 喜欢的可以 点赞 收藏 关注哦~~ ✍️ 作者简介: 一个热爱大数据的学习者 文章目录 目录 文章目录 简介 if循环 if...else 语句 if...else if...else 语句 if语句嵌套 switch 语句 嵌套 switch 语句 简介 顺序结构&am…

快速搭建Electron+Vite3+Vue3+TypeScript5脚手架 (无需梯子,快速安装Electron)

一、介绍 &#x1f606; &#x1f601; &#x1f609; Electron是一个使用 JavaScript、HTML 和 CSS 构建桌面应用程序的框架。 嵌入 Chromium 和 Node.js 到 二进制的 Electron 允许您保持一个 JavaScript 代码代码库并创建 在Windows上运行的跨平台应用 macOS和Linux——不需…

【网络安全】红队基础免杀

引言 本文主要介绍“反射型 dll 注入”及“柔性加载”技术。 反射型 dll 注入 为什么需要反射型 dll 注入 常规的 dll 注入代码如下&#xff1a; int main(int argc, char *argv[]) {HANDLE processHandle;PVOID remoteBuffer;wchar_t dllPath[] TEXT("C:\\experime…

通信原理简答题

目录 1&#xff0e; 通信系统由哪些部分组成&#xff1f;各组成部分的作用是什么&#xff1f; 2&#xff0e; 当谐振功率放大器的输入激励信号为余弦波时&#xff0c;为什么集电极电流为余弦脉冲波形&#xff1f;但放大器为什么又能输出不失真的余弦波电压&#xff1f; 3&am…

如何完全卸载linux下通过rpm安装的mysql

卸载linux下通过rpm安装的mysql 1.关闭MySQL服务2.使用 rpm 命令的方式查看已安装的mysql3. 使用rpm -ev 命令移除安装4. 查询是否还存在遗漏文件5. 删除MySQL数据库内容 1.关闭MySQL服务 如果之前安装过并已经启动&#xff0c;则需要卸载前请先关闭MySQL服务 systemctl stop…

程序员为什么越来越内卷了?聊聊java架构师需要掌握的技巧

几年前&#xff0c;一个其他专业的本科生或者专科生&#xff0c;随便培训几个月&#xff0c;便可以轻易拿到1万以上的月薪。培训机构可以拿出很多强有力的证据来证明这个行业的前景非常可观&#xff0c;这个趋势吸引了越来越多的人争相涌入到这个行业。正是因为人员越来越趋于饱…

无人机集群路径规划:淘金优化算法(Gold rush optimizer,GRO)提供MATLAB代码

一、淘金优化算法GRO 淘金优化算法&#xff08;Gold rush optimizer&#xff0c;GRO&#xff09;由Kamran Zolf于2023年提出&#xff0c;其灵感来自淘金热&#xff0c;模拟淘金者进行黄金勘探行为。 参考文献&#xff1a; K. Zolfi. Gold rush optimizer: A new population-ba…

分治算法:原理、实现及应用场景分析

分治算法是一种常用的算法思想,他将问题分成两个或多个更小的子问题,直到最小的子问题可以直接解决,然后将子问题的解合并成原始问题的解。下面将详细介绍分治算法的原理与实现,以及其优缺点与应用场景。 1、原理 分治算法的思想在古代就已经被儒家思想所包含:“尽人事,…
最新文章