当前位置: 首页 > news >正文

玩转Mysql系列 - 第26篇:聊聊mysql如何实现分布式锁?

这是Mysql系列第26篇。

本篇我们使用mysql实现一个分布式锁。

分布式锁的功能

  1. 分布式锁使用者位于不同的机器中,锁获取成功之后,才可以对共享资源进行操作

  2. 锁具有重入的功能:即一个使用者可以多次获取某个锁

  3. 获取锁有超时的功能:即在指定的时间内去尝试获取锁,超过了超时时间,如果还未获取成功,则返回获取失败

  4. 能够自动容错,比如:A机器获取锁lock1之后,在释放锁lock1之前,A机器挂了,导致锁lock1未释放,结果会lock1一直被A机器占有着,遇到这种情况时,分布式锁要能够自动解决,可以这么做:持有锁的时候可以加个持有超时时间,超过了这个时间还未释放的,其他机器将有机会获取锁

预备技能:乐观锁

通常我们修改表中一条数据过程如下:

t1:select获取记录R1
t2:对R1进行编辑
t3:update R1

我们来看一下上面的过程存在的问题:

如果A、B两个线程同时执行到t1,他们俩看到的R1的数据一样,然后都对R1进行编辑,然后去执行t3,最终2个线程都会更新成功,后面一个线程会把前面一个线程update的结果给覆盖掉,这就是并发修改数据存在的问题。

我们可以在表中新增一个版本号,每次更新数据时候将版本号作为条件,并且每次更新时候版本号+1,过程优化一下,如下:

t1:打开事务start transaction
t2:select获取记录R1,声明变量v=R1.version
t3:对R1进行编辑
t4:执行更新操作update R1 set version = version + 1 where user_id=#user_id# and version = #v#;
t5:t4中的update会返回影响的行数,我们将其记录在count中,然后根据count来判断提交还是回滚if(count==1){//提交事务commit;}else{//回滚事务rollback;}

上面重点在于步骤t4,当多个线程同时执行到t1,他们看到的R1是一样的,但是当他们执行到t4的时候,数据库会对update的这行记录加锁,确保并发情况下排队执行,所以只有第一个的update会返回1,其他的update结果会返回0,然后后面会判断count是否为1,进而对事务进行提交或者回滚。可以通过count的值知道修改数据是否成功了。

上面这种方式就乐观锁。我们可以通过乐观锁的方式确保数据并发修改过程中的正确性。

使用mysql实现分布式锁

建表

我们创建一个分布式锁表,如下

DROP DATABASE IF EXISTS javacode2018;
CREATE DATABASE javacode2018;
USE javacode2018;
DROP TABLE IF EXISTS t_lock;
create table t_lock(lock_key varchar(32) PRIMARY KEY NOT NULL COMMENT '锁唯一标志',request_id varchar(64) NOT NULL DEFAULT '' COMMENT '用来标识请求对象的',lock_count INT NOT NULL DEFAULT 0 COMMENT '当前上锁次数',timeout BIGINT NOT NULL DEFAULT 0 COMMENT '锁超时时间',version INT NOT NULL DEFAULT 0 COMMENT '版本号,每次更新+1'
)COMMENT '锁信息表';
分布式锁工具类:
package com.itsoku.sql;import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;import java.sql.*;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;/*** 工作10年的前阿里P7分享Java、算法、数据库方面的技术干货!坚信用技术改变命运,让家人过上更体面的生活!* 喜欢的请关注公众号:路人甲Java*/
@Slf4j
public class LockUtils {//将requestid保存在该变量中static ThreadLocal<String> requestIdTL = new ThreadLocal<>();/*** 获取当前线程requestid** @return*/public static String getRequestId() {String requestId = requestIdTL.get();if (requestId == null || "".equals(requestId)) {requestId = UUID.randomUUID().toString();requestIdTL.set(requestId);}log.info("requestId:{}", requestId);return requestId;}/*** 获取锁** @param lock_key        锁key* @param locktimeout(毫秒) 持有锁的有效时间,防止死锁* @param gettimeout(毫秒)  获取锁的超时时间,这个时间内获取不到将重试* @return*/public static boolean lock(String lock_key, long locktimeout, int gettimeout) throws Exception {log.info("start");boolean lockResult = false;String request_id = getRequestId();long starttime = System.currentTimeMillis();while (true) {LockModel lockModel = LockUtils.get(lock_key);if (Objects.isNull(lockModel)) {//插入一条记录,重新尝试获取锁LockUtils.insert(LockModel.builder().lock_key(lock_key).request_id("").lock_count(0).timeout(0L).version(0).build());} else {String reqid = lockModel.getRequest_id();//如果reqid为空字符,表示锁未被占用if ("".equals(reqid)) {lockModel.setRequest_id(request_id);lockModel.setLock_count(1);lockModel.setTimeout(System.currentTimeMillis() + locktimeout);if (LockUtils.update(lockModel) == 1) {lockResult = true;break;}} else if (request_id.equals(reqid)) {//如果request_id和表中request_id一样表示锁被当前线程持有者,此时需要加重入锁lockModel.setTimeout(System.currentTimeMillis() + locktimeout);lockModel.setLock_count(lockModel.getLock_count() + 1);if (LockUtils.update(lockModel) == 1) {lockResult = true;break;}} else {//锁不是自己的,并且已经超时了,则重置锁,继续重试if (lockModel.getTimeout() < System.currentTimeMillis()) {LockUtils.resetLock(lockModel);} else {//如果未超时,休眠100毫秒,继续重试if (starttime + gettimeout > System.currentTimeMillis()) {TimeUnit.MILLISECONDS.sleep(100);} else {break;}}}}}log.info("end");return lockResult;}/*** 释放锁** @param lock_key* @throws Exception*/public static void unlock(String lock_key) throws Exception {//获取当前线程requestIdString requestId = getRequestId();LockModel lockModel = LockUtils.get(lock_key);//当前线程requestId和库中request_id一致 && lock_count>0,表示可以释放锁if (Objects.nonNull(lockModel) && requestId.equals(lockModel.getRequest_id()) && lockModel.getLock_count() > 0) {if (lockModel.getLock_count() == 1) {//重置锁resetLock(lockModel);} else {lockModel.setLock_count(lockModel.getLock_count() - 1);LockUtils.update(lockModel);}}}/*** 重置锁** @param lockModel* @return* @throws Exception*/public static int resetLock(LockModel lockModel) throws Exception {lockModel.setRequest_id("");lockModel.setLock_count(0);lockModel.setTimeout(0L);return LockUtils.update(lockModel);}/*** 更新lockModel信息,内部采用乐观锁来更新** @param lockModel* @return* @throws Exception*/public static int update(LockModel lockModel) throws Exception {return exec(conn -> {String sql = "UPDATE t_lock SET request_id = ?,lock_count = ?,timeout = ?,version = version + 1 WHERE lock_key = ? AND  version = ?";PreparedStatement ps = conn.prepareStatement(sql);int colIndex = 1;ps.setString(colIndex++, lockModel.getRequest_id());ps.setInt(colIndex++, lockModel.getLock_count());ps.setLong(colIndex++, lockModel.getTimeout());ps.setString(colIndex++, lockModel.getLock_key());ps.setInt(colIndex++, lockModel.getVersion());return ps.executeUpdate();});}public static LockModel get(String lock_key) throws Exception {return exec(conn -> {String sql = "select * from t_lock t WHERE t.lock_key=?";PreparedStatement ps = conn.prepareStatement(sql);int colIndex = 1;ps.setString(colIndex++, lock_key);ResultSet rs = ps.executeQuery();if (rs.next()) {return LockModel.builder().lock_key(lock_key).request_id(rs.getString("request_id")).lock_count(rs.getInt("lock_count")).timeout(rs.getLong("timeout")).version(rs.getInt("version")).build();}return null;});}public static int insert(LockModel lockModel) throws Exception {return exec(conn -> {String sql = "insert into t_lock (lock_key, request_id, lock_count, timeout, version) VALUES (?,?,?,?,?)";PreparedStatement ps = conn.prepareStatement(sql);int colIndex = 1;ps.setString(colIndex++, lockModel.getLock_key());ps.setString(colIndex++, lockModel.getRequest_id());ps.setInt(colIndex++, lockModel.getLock_count());ps.setLong(colIndex++, lockModel.getTimeout());ps.setInt(colIndex++, lockModel.getVersion());return ps.executeUpdate();});}public static <T> T exec(SqlExec<T> sqlExec) throws Exception {Connection conn = getConn();try {return sqlExec.exec(conn);} finally {closeConn(conn);}}@FunctionalInterfacepublic interface SqlExec<T> {T exec(Connection conn) throws Exception;}@Getter@Setter@Builderpublic static class LockModel {private String lock_key;private String request_id;private Integer lock_count;private Long timeout;private Integer version;}private static final String url = "jdbc:mysql://localhost:3306/javacode2018?useSSL=false";        //数据库地址private static final String username = "root";        //数据库用户名private static final String password = "root123";        //数据库密码private static final String driver = "com.mysql.jdbc.Driver";        //mysql驱动/*** 连接数据库** @return*/public static Connection getConn() {Connection conn = null;try {Class.forName(driver);  //加载数据库驱动try {conn = DriverManager.getConnection(url, username, password);  //连接数据库} catch (SQLException e) {e.printStackTrace();}} catch (ClassNotFoundException e) {e.printStackTrace();}return conn;}/*** 关闭数据库链接** @return*/public static void closeConn(Connection conn) {if (conn != null) {try {conn.close();  //关闭数据库链接} catch (SQLException e) {e.printStackTrace();}}}
}

上面代码中实现了文章开头列的分布式锁的所有功能,大家可以认真研究下获取锁的方法:lock,释放锁的方法:unlock

测试用例
package com.itsoku.sql;import lombok.extern.slf4j.Slf4j;
import org.junit.Test;import static com.itsoku.sql.LockUtils.lock;
import static com.itsoku.sql.LockUtils.unlock;/*** 工作10年的前阿里P7分享Java、算法、数据库方面的技术干货!坚信用技术改变命运,让家人过上更体面的生活!* 喜欢的请关注公众号:路人甲Java*/
@Slf4j
public class LockUtilsTest {//测试重复获取和重复释放@Testpublic void test1() throws Exception {String lock_key = "key1";for (int i = 0; i < 10; i++) {lock(lock_key, 10000L, 1000);}for (int i = 0; i < 9; i++) {unlock(lock_key);}}//获取之后不释放,超时之后被thread1获取@Testpublic void test2() throws Exception {String lock_key = "key2";lock(lock_key, 5000L, 1000);Thread thread1 = new Thread(() -> {try {try {lock(lock_key, 5000L, 7000);} finally {unlock(lock_key);}} catch (Exception e) {e.printStackTrace();}});thread1.setName("thread1");thread1.start();thread1.join();}
}

test1方法测试了重入锁的效果。

test2测试了主线程获取锁之后一直未释放,持有锁超时之后被thread1获取到了。

留给大家一个问题

上面分布式锁还需要考虑一个问题:比如A机会获取了key1的锁,并设置持有锁的超时时间为10秒,但是获取锁之后,执行了一段业务操作,业务操作耗时超过10秒了,此时机器B去获取锁时可以获取成功的,此时会导致A、B两个机器都获取锁成功了,都在执行业务操作,这种情况应该怎么处理?大家可以思考一下然后留言,我们一起讨论一下。

相关文章:

玩转Mysql系列 - 第26篇:聊聊mysql如何实现分布式锁?

这是Mysql系列第26篇。 本篇我们使用mysql实现一个分布式锁。 分布式锁的功能 分布式锁使用者位于不同的机器中&#xff0c;锁获取成功之后&#xff0c;才可以对共享资源进行操作 锁具有重入的功能&#xff1a;即一个使用者可以多次获取某个锁 获取锁有超时的功能&#xff…...

linux 解压缩命令tar

Tar tar 命令的选项有很多(用 man tar 可以查看到)&#xff0c;但常用的就那么几个选项&#xff0c;下面来举例说明一下&#xff1a; tar -cf all.tar *.jpg 这条命令是将所有.jpg 的文件打成一个名为 all.tar 的包。-c 是表示产生新的包&#xff0c;-f 指 定包的文件名。 …...

OpenAI ChatGPT API 文档之 Embedding

译者注&#xff1a; Embedding 直接翻译为嵌入似乎不太恰当&#xff0c;于是问了一下 ChatGPT&#xff0c;它的回复如下&#xff1a; 在自然语言处理和机器学习领域&#xff0c;"embeddings" 是指将单词、短语或文本转换成连续向量空间的过程。这个向量空间通常被称…...

Java常用类(二)

好久不见&#xff0c;因工作原因&#xff0c;好久没发文了&#xff0c;OldWang 回来了&#xff0c;持续更新Java内容&#xff01;⭐ 不可变和可变字符序列使用陷阱⭐ 时间处理相关类⭐ Date 时间类(java.util.Date)⭐ DateFormat 类和 SimpleDateFormat 类⭐ Calendar 日历类 ⭐…...

Java获取给定月份的前N个月份和前N个季度

描述&#xff1a; 在项目开发过程中&#xff0c;遇到这样一个需求&#xff0c;即&#xff1a;给定某一月份&#xff0c;得到该月份前面的几个月份以及前面的几个季度。例如&#xff1a;给定2023-09&#xff0c;获取该月份前面的前3个月&#xff0c;即2023-08、2023-07、2023-0…...

网页资源加载过程

网页资源加载是指在浏览器中访问一个网页时&#xff0c;浏览器如何获取和显示网页内容的过程。这个过程通常分为以下几个步骤&#xff1a; DNS 解析&#xff1a; 当用户在浏览器中输入一个网址&#xff08;例如&#xff0c;https://www.example.com&#xff09;&#xff0c;浏览…...

使用git config --global设置用户名和邮件,以及git config的全局和局部配置

文章目录 1. 文章引言2. 全局配置2.1 命令方式2.2 配置文件方式 3. 局部配置3.1 命令方式3.2 配置文件方式 4. 总结 1. 文章引言 我们为什么要设置设置用户名和邮件&#xff1f; 我们在注册github&#xff0c;gitlab等时&#xff0c;一般使用用户名或邮箱&#xff1a; 这个用户…...

【C语言】21-指针-3

目录 1. 指针数组1.1 什么是指针数组1.2 如何定义指针数组1.3 如何使用指针数组2. 多重指针2.1 二重指针的定义2.2 二重指针的初始化与赋值2.3 二重指针的使用3. 指针常量、常量指针、指向常量的常指针3.1 概念3.2 const pointer3.3 pointer to a constant3.3.1 (pointer to a …...

解决craco启动react项目卡死在Starting the development server的问题

现象&#xff1a; 原因&#xff1a;craco.config.ts配置文件有问题 经过排查发现Dev开发模式下不能有splitChunk的配置&#xff0c; 解决办法&#xff1a; 加一个生产模式的判断&#xff0c;开发模式不加载splitChunk的配置&#xff0c;仅在生产模式才加载 判断条件代码&#…...

常见的密码学算法都有哪些?

密码学算法是用于保护信息安全的数学方法和技术。它们可以分为多个类别&#xff0c;包括对称加密、非对称加密、哈希函数和数字签名等。以下是一些常见的密码学算法&#xff1a; 1、对称加密算法&#xff1a; AES&#xff08;高级加密标准&#xff09;&#xff1a;一种广泛使…...

云安全【阿里云ECS攻防】

关于VPC的概念还请看&#xff1a;记录一下弹性计算云服务的一些词汇概念 - 火线 Zone-安全攻防社区 一、初始化访问 1、元数据 1.1、SSRF导致读取元数据 如果管理员给ECS配置了RAM角色&#xff0c;那么就可以获得临时凭证 如果配置RAM角色 在获取ram临时凭证的时候&#xff…...

TBSS数据分析

tbss分析基本流程&#xff1a; 步骤一&#xff0c;指标解算&#xff1a;求解出FA&#xff0c;MD&#xff0c;AD&#xff0c;RD指标 #!/bin/bash #基于体素的形态学分析VBA path/media/kui/Passport5T/DATA_help/TBSS/row_data mkdir ${path}/Results_DTI_tbss mkdir ${path}/R…...

【单调队列】 239. 滑动窗口最大值

239. 滑动窗口最大值 解题思路 计算每一个滑动窗口的最大值 关键在于借助单调队列实现窗口对于单调队列 尾部添加元素 头部删除元素添加元素操作&#xff1a;从尾部开始循环对比 删除比当前元素小的元素获取最大值元素 直接获取头部元素删除元素操作 直接删除头部元素 class…...

Spring实例化源码解析之ComponentScanAnnotationParser(四)

上一章我们分析了ConfigurationClassParser&#xff0c;配置类的解析源码分析。在ComponentScans和ComponentScan注解修饰的候选配置类的解析过程中&#xff0c;我们需要深入的了解一下ComponentScanAnnotationParser的parse执行流程&#xff0c;SpringBoot启动类为什么这么写&…...

MySQL - 外键(foreign key)约束的作用和使用

什么是外键约束&#xff1f; 外键&#xff1a;用来让两张表的数据之间建立连接&#xff0c;从而保证数据的一致性和完整性。 外键约束是用于建立两个表之间关系的一种约束&#xff0c;它定义了一个表中的列与另一个表中的列之间的关系。外键约束可以保证数据的完整性和一致性…...

前端开发之服务器的基本概念与初识Ajax

1&#xff0c;服务器的基本概念与初识Ajax 1.1 URL地址的组成部分 1.2 客户端与服务器的通信过程 1.3 网页中如何请求数据 1.4 $.get()函数 1.4.1 $.get()函数的语法 // jQuery 中 $.get() 函数的功能单一&#xff0c;专门用来发起 get 请求&#xff0c;从而将服务器上的资源…...

数据结构排序算法---八大排序复杂度及代码实现

文章目录 一、冒泡排序代码实现 二、直接插入排序代码实现 三、希尔排序代码实现 四、选择排序代码实现 五、堆排序代码实现 六、快速排序代码实现 七、归并排序代码实现 八、计数排序代码实现 稳定性&#xff1a;相同的数据排序后&#xff0c;相对位置是否发生改变 一、冒泡排…...

GMS之Launcher中去除默认Search或替换为Chrome Search

将Launcher中搜索框去除 将FeatureFlags.java文件中的QSB_ON_FIRST_SCREEN变量修改为false \system\vendor\mediatek\proprietary\packages\apps\Launcher3\src\com\android\launcher3\config\FeatureFlags.java/*** Defines a set of flags used to control various launche…...

@DateTimeFormat 和 @JsonFormat 的详细研究

关于这两个时间转化注解&#xff0c;先说结论 一、介绍 1、DateTimeFormat DateTimeFormat 并不会根据得到其属性 pattern 把前端传入的数据转换成自己想要的格式&#xff0c;而是将前端的String类型数据封装到Date类型&#xff1b;其次它的 pattern 属性是用来规范前端传入…...

nodejs基于Vue.js健身体育器材用品商城购物网97794

管理员端的功能主要是开放给系统的管理人员使用&#xff0c;能够对用户的信息进行管理&#xff0c;包括对用户、健身器材、器材类型、系统和订单进行查看&#xff0c;修改和删除、新增等&#xff0c;对系统整体运行情况进行了解。用户的功能主要是对个人账号和密码进行更新信息…...

理解 MCP 工作流:使用 Ollama 和 LangChain 构建本地 MCP 客户端

&#x1f31f; 什么是 MCP&#xff1f; 模型控制协议 (MCP) 是一种创新的协议&#xff0c;旨在无缝连接 AI 模型与应用程序。 MCP 是一个开源协议&#xff0c;它标准化了我们的 LLM 应用程序连接所需工具和数据源并与之协作的方式。 可以把它想象成你的 AI 模型 和想要使用它…...

【Java_EE】Spring MVC

目录 Spring Web MVC ​编辑注解 RestController RequestMapping RequestParam RequestParam RequestBody PathVariable RequestPart 参数传递 注意事项 ​编辑参数重命名 RequestParam ​编辑​编辑传递集合 RequestParam 传递JSON数据 ​编辑RequestBody ​…...

C++ 求圆面积的程序(Program to find area of a circle)

给定半径r&#xff0c;求圆的面积。圆的面积应精确到小数点后5位。 例子&#xff1a; 输入&#xff1a;r 5 输出&#xff1a;78.53982 解释&#xff1a;由于面积 PI * r * r 3.14159265358979323846 * 5 * 5 78.53982&#xff0c;因为我们只保留小数点后 5 位数字。 输…...

IT供电系统绝缘监测及故障定位解决方案

随着新能源的快速发展&#xff0c;光伏电站、储能系统及充电设备已广泛应用于现代能源网络。在光伏领域&#xff0c;IT供电系统凭借其持续供电性好、安全性高等优势成为光伏首选&#xff0c;但在长期运行中&#xff0c;例如老化、潮湿、隐裂、机械损伤等问题会影响光伏板绝缘层…...

[Java恶补day16] 238.除自身以外数组的乘积

给你一个整数数组 nums&#xff0c;返回 数组 answer &#xff0c;其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。 题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。 请 不要使用除法&#xff0c;且在 O(n) 时间复杂度…...

HarmonyOS运动开发:如何用mpchart绘制运动配速图表

##鸿蒙核心技术##运动开发##Sensor Service Kit&#xff08;传感器服务&#xff09;# 前言 在运动类应用中&#xff0c;运动数据的可视化是提升用户体验的重要环节。通过直观的图表展示运动过程中的关键数据&#xff0c;如配速、距离、卡路里消耗等&#xff0c;用户可以更清晰…...

LLaMA-Factory 微调 Qwen2-VL 进行人脸情感识别(二)

在上一篇文章中,我们详细介绍了如何使用LLaMA-Factory框架对Qwen2-VL大模型进行微调,以实现人脸情感识别的功能。本篇文章将聚焦于微调完成后,如何调用这个模型进行人脸情感识别的具体代码实现,包括详细的步骤和注释。 模型调用步骤 环境准备:确保安装了必要的Python库。…...

保姆级【快数学会Android端“动画“】+ 实现补间动画和逐帧动画!!!

目录 补间动画 1.创建资源文件夹 2.设置文件夹类型 3.创建.xml文件 4.样式设计 5.动画设置 6.动画的实现 内容拓展 7.在原基础上继续添加.xml文件 8.xml代码编写 (1)rotate_anim (2)scale_anim (3)translate_anim 9.MainActivity.java代码汇总 10.效果展示 逐帧…...

热门Chrome扩展程序存在明文传输风险,用户隐私安全受威胁

赛门铁克威胁猎手团队最新报告披露&#xff0c;数款拥有数百万活跃用户的Chrome扩展程序正在通过未加密的HTTP连接静默泄露用户敏感数据&#xff0c;严重威胁用户隐私安全。 知名扩展程序存在明文传输风险 尽管宣称提供安全浏览、数据分析或便捷界面等功能&#xff0c;但SEMR…...

LINUX编译vlc

下载 VideoLAN / VLC GitLab 选择最新的发布版本 准备 sudo apt install -y xcb bison sudo apt install -y autopoint sudo apt install -y autoconf automake libtool编译ffmpeg LINUX FFMPEG编译汇总&#xff08;最简化&#xff09;_底部的附件列表中】: ffmpeg - lzip…...