MySql如何实现分布式锁
本篇我们使用mysql实现一个分布式锁。
环境:mysql8,navicat,maven,springboot2.3.11,mybatis-plus
分布式锁的功能
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 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 '锁信息表';
java代码如下
mapper接口
package com.shiguiwu.springmybatis.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.shiguiwu.springmybatis.lock.model.LockModel;
import org.springframework.stereotype.Repository;/*** @description: 锁mapper* @author: stone* @date: Created by 2021/5/30 11:12* @version: 1.0.0* @pakeage: com.shiguiwu.springmybatis.mapper*/
@Repository
public interface LockMapper extends BaseMapper<LockModel> {}
锁对象model
package com.shiguiwu.springmybatis.lock.model;import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.Version;
import lombok.Data;/*** @description: 锁模型* @author: stone* @date: Created by 2021/9/10 11:13* @version: 1.0.0* @pakeage: com.shiguiwu.springmybatis.lock.model*/
@Data
@TableName("t_lock")
public class LockModel {/*** 锁的唯一值*/@TableIdprivate String lockKey;/*** 请求id,同一个线程里请求id一样*/private String requestId;//锁次数private Integer lockCount;//锁超时private Long timeout;//乐观锁版本@Versionprivate Integer version;
}
锁接口
package com.shiguiwu.springmybatis.lock;/*** @description: 锁接口* @author: stone* @date: Created by 2021/9/10 11:40* @version: 1.0.0* @pakeage: com.shiguiwu.springmybatis.lock*/
public interface ILock<T> {/*** 获取分布式锁,支持重入* @param lockKey 锁可以* @param lockTimeout 持有锁的有效时间,防止死锁* @param getTimeout 获取锁超时时间,* @return 是否锁成功*/public boolean lock(String lockKey, long lockTimeout, int getTimeout) throws Exception;/*** 解锁* @param lockKey 锁key**/public void unlock(String lockKey);/*** 重置锁对象* @param t 锁对象* @return 返回锁记录*/public int restLock(T t);}
锁的实现代码如下
package com.shiguiwu.springmybatis.lock;import cn.hutool.core.util.StrUtil;
import com.shiguiwu.springmybatis.lock.model.LockModel;
import com.shiguiwu.springmybatis.mapper.LockMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;/*** @description: mysql实现分布式锁* @author: stone* @date: Created by 2021/9/10 11:09* @version: 1.0.0* @pakeage: com.shiguiwu.springmybatis.lock*/
@Component
@Slf4j
public class MysqlLock implements ILock<LockModel>{static ThreadLocal<String> requestIds = new ThreadLocal<>();@Autowiredprivate LockMapper lockMapper;public String getRequestId() {String requestId = requestIds.get();if (StrUtil.isBlank(requestId)) {requestId = UUID.randomUUID().toString();requestIds.set(requestId);}log.info("获取到的requestId===> {}", requestId);return requestId;}/*** 获取锁* @param lockKey 锁可以* @param lockTimeout 持有锁的有效时间,防止死锁* @param getTimeout 获取锁超时时间,* @return*/@Overridepublic boolean lock(String lockKey, long lockTimeout, int getTimeout) throws Exception {log.info(" lock start =======================> {}",lockKey);//从local中获取 请求idString requestId = this.getRequestId();//获取锁的结果boolean lockResult = false;//开始时间long startTime = System.currentTimeMillis();while (true) {LockModel lockModel = lockMapper.selectById(lockKey);if (Objects.nonNull(lockModel)) {//获取锁对象的请求idString reqId = lockModel.getRequestId();//如果是空,表示改锁未被占有if (StrUtil.isBlank(reqId)) {//马上占有它//设置请求idlockModel.setRequestId(requestId);//设置锁次数lockModel.setLockCount(1);//设置超时时间,防止死锁lockModel.setTimeout(System.currentTimeMillis() + lockTimeout);if (lockMapper.updateById(lockModel) == 1) {lockResult = true;break;}}//如果request_id和表中request_id一样表示锁被当前线程持有者,此时需要加重入锁else if (requestId.equals(reqId)) {//可重入锁lockModel.setTimeout(System.currentTimeMillis() + lockTimeout);//设置获取初次lockModel.setLockCount(lockModel.getLockCount() + 1);if (lockMapper.updateById(lockModel) == 1) {lockResult = true;break;}}//不为空,也不相等,说明是其他线程占有else {//锁不是自己的,并且已经超时了,则重置锁,继续重试if (lockModel.getTimeout() < System.currentTimeMillis()) {//未超时,继续重试this.restLock(lockModel);}//如果未超时,休眠100毫秒,继续重试else {if (startTime + getTimeout > System.currentTimeMillis()) {TimeUnit.MILLISECONDS.sleep(100);}else {//防止长时间阻塞break;}}}}//如果是空,就插入一个锁,重新尝试获取锁else {lockModel = new LockModel();//设置锁keylockModel.setLockKey(lockKey);lockMapper.insert(lockModel);}}log.info(" lock end =======================> {}",lockKey);return lockResult;}/*** 释放锁* @param lockKey 锁key*/@Overridepublic void unlock(String lockKey) {LockModel lockModel = lockMapper.selectById(lockKey);//获取当前线程的请求idString reqId = this.getRequestId();//获取锁次数int count = 0;//当前线程requestId和库中request_id一致 && lock_count>0,表示可以释放锁if (Objects.nonNull(lockModel)&& reqId.equals(lockModel.getRequestId())&& (count = lockModel.getLockCount()) > 0) {if (count == 1) {//重置锁this.restLock(lockModel);}//重入锁的问题,锁的次数减一else {lockModel.setLockCount(lockModel.getLockCount() - 1);//更新次数lockMapper.updateById(lockModel);}}}/*** 重置锁* @param lockModel 锁对象* @return 更新条数*/@Overridepublic int restLock(LockModel lockModel) {lockModel.setLockCount(0);lockModel.setRequestId("");lockModel.setTimeout(0L);return lockMapper.updateById(lockModel);}}
上面代码中实现了文章开头列的分布式锁的所有功能,大家可以认真研究下获取锁的方法:lock,释放锁的方法:unlock。
测试用例
package com.shiguiwu.springmybatis;import com.shiguiwu.springmybatis.lock.ILock;
import com.shiguiwu.springmybatis.lock.model.LockModel;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;/*** @description: 锁测试* @author: stone* @date: Created by 2021/9/10 15:32* @version: 1.0.0* @pakeage: com.shiguiwu.springmybatis*/
@SpringBootTest
@Slf4j
public class LockApplicationTests {@Autowiredprivate ILock<LockModel> mysqlLock;测试重复获取和重复释放@Testpublic void testRepeat() throws Exception {for (int i = 0; i < 10; i++) {mysqlLock.lock("key1", 10000L, 1000);}for (int i = 0; i < 10; i++) {mysqlLock.unlock("key1");}}// //获取之后不释放,超时之后被thread1获取@Testpublic void testTimeout() throws Exception {String lockKey = "key2";mysqlLock.lock(lockKey, 5000L, 1000);Thread thread1 = new Thread(() -> {try {mysqlLock.lock(lockKey, 5000L, 7000);} catch (Exception e) {e.printStackTrace();} finally {mysqlLock.unlock(lockKey);}}, "thread1");thread1.start();thread1.join();}}
test1方法测试了重入锁的效果。
test2测试了主线程获取锁之后一直未释放,持有锁超时之后被thread1获取到了
留给大家一个问题
上面分布式锁还需要考虑一个问题:比如A机会获取了key1的锁,并设置持有锁的超时时间为10秒,但是获取锁之后,执行了一段业务操作,业务操作耗时超过10秒了,此时机器B去获取锁时可以获取成功的,此时会导致A、B两个机器都获取锁成功了,都在执行业务操作,这种情况应该怎么处理?大家可以思考一下然后留言,我们一起讨论一下。
相关文章:
MySql如何实现分布式锁
本篇我们使用mysql实现一个分布式锁。 环境:mysql8,navicat,maven,springboot2.3.11,mybatis-plus 分布式锁的功能 1,分布式锁使用者位于不同的机器中,锁获取成功之后,才可以对共享资源进行操作 2,锁具有重入的功能:即一个使用…...

「行内揭秘」 SQLynx数据库界的“小众宝藏”?
数据库界的“小众宝藏”?Navicat老大哥地位稳如泰山,但这位“SQLynx”小弟也不容小觑!👀 别看它小众,SQLynx在处理数据库事务上那可是丝毫不含糊,无论你是Windows Linux和Mac,甚至银河麒麟统信都…...

【已解决】【MySQL】IDEA配置数据库 报错 未配置SQL方言 无法使用SQL提示
IDEA配置数据库的步骤 下载插件 添加数据源 新建--->选择数据源MySQL 页面展示: 主机名:一般都是localhost不用改端口:填写自己的端口号用户:填写自己的用户名密码:填写自己设置的密码数据库:填写需要…...

Android 开发 调节声音 SeekBar自定义样式
效果图 xml布局 mipmap/seekbar图片随意一张图都可以,这里我的图就不贴出来了 <SeekBarandroid:id"id/seekBar"android:layout_marginLeft"8dp"android:layout_width"377dp"android:layout_height"8dp"android:layou…...

UART-通用异步收发器
1. UART的基本工作原理 UART通信主要有两个部分构成:发送器和接收器,也就是我们常见的(RX接收,TX发送)两个独立的线路来实现数据的双向传输,由于是异步的,UART并不需要时钟信号,而是…...

Linux——— 信号
文章目录 前言:引入信号生活中的例子信号概念见一见Linux中的信号 浅度理解信号信号处理(浅谈):如何自定义捕捉 信号保存(浅谈) 信号产生系统调用产生异常产生:浅谈除0异常浅谈解引用野指针异常Core &&…...
安全见闻-web安全
web安全 一、web程序简介 1. Web程序的基本构成 2. 工作流程 3. 安全性 二、JavaScript代码库 1. 代码库的概念和用途 2. 常见的代码库 三、框架 1. 常见的前端框架 2. 常见的后端框架 四、数据库 1. 数据库的分类 2. 数据库的潜在漏洞 3. 学习数据库的重要性 五、…...

华为手机卸载系统应用的方法
摘要: 1.手机环境:手机需要开启开发者模式并使用usb连接电脑,并选择文件传输模式 2.电脑环境:使用鸿蒙工具箱进行傻瓜操作或安装adb工具进行命令卸载 3.鸿蒙工具箱和adb工具本质都是使用adb shell pm uninstall -k --user 0 xx…...
力扣算法笔记——生成随机数组
题目信息: 给两个随机数和N,生成M到N的随机不重复数组,且M<N。 示例:输入M 2, N5, 输出 [4,3,2,5]. 思路:洗牌算法,先遍历M到N之间所有的数字,将得到的结果存入一个集合中,将集合从后往前遍历…...

Anaconda和Pycharm超详细安装教程(2024版本+Win11)
详细安装:https://download.csdn.net/download/qq_40379132/89924782 一、安装Anaconda 1.1 下载Anaconda 在官方网站(Free Download | Anaconda)上下载适用于你的操作系统的 Anaconda 安装包。(这里以windows为例)…...
代码随想录:从中后/中前遍历序列构造二叉树
106. 从中序与后序遍历序列构造二叉树 用分治思想,后序遍历是左右中,中序遍历是左中右,后序遍历的最后一个元素就是根节点, 在中序遍历中找到它的位置,它前面的为左子树,后面的为右子树,并能计…...

2-134 基于matlab的图像边缘检测
基于matlab的图像边缘检测,采用六种算子(分别是gabor、拉普拉斯、priwitt、robert、sobel、wallis微分算子),对图象进行边缘检测比较,输出边缘检测结果。可对比效果优劣。程序已调通,可直接运行。 下载源程序请点链接…...

【Java并发编程】线程池详解
一、简介 随着计算机行业的飞速发展,摩尔定律逐渐失效,多核CPU成为主流。使用多线程并行计算逐渐成为开发人员提升服务器性能的基本武器。J.U.C提供的线程池:ThreadPoolExecutor 类,帮助开发人员管理线程并方便地执行并行任务。了…...

ThingsBoard规则链节点:GPS Geofencing Events节点详解
引言 1. GPS Geofencing Events 节点简介 2. 节点配置 3. 使用场景 3.1 物流跟踪 3.2 资产管理 3.3 安全监控 3.4 农业监测 4. 实际项目中的应用 4.1 项目背景 4.2 项目需求 4.3 实现步骤 5. 总结 引言 GPS Geofencing Events 是 ThingsBoard 规则链中的一个重要节…...

Jmeter基础篇(19)JSR223预处理器
前言 JSR223预处理器是Apache JMeter中的一个组件,它允许用户使用任何支持Java Scripting API (JSR 223) 的脚本语言来执行预处理任务。这个功能非常强大,因为它让测试人员能够利用如Groovy、JavaScript(Nashorn引擎)、BeanShell…...
通过js控制css变量
在JavaScript中,你可以通过操作CSS变量(也称为自定义属性)来动态改变样式。CSS变量在CSS中使用 – 前缀定义,例如 --main-color: red;。在JavaScript中,你可以使用 document.documentElement.style.setProperty 方法来…...

Docker:容器化和虚拟化
虚拟化 虚拟化是一种资源管理技术,它将计算机的各种实体资源(如CPU、内存、磁盘空间、网络适配器等)予以抽象、转换后呈现出来,并可供分割、组合为一个或多个电脑配置环境。这些资源的新虚拟部分是不受现有资源的架设方式、地域或…...

OpenSSL
OpenSSL 概述 OpenSSL 是一个开源的、安全传输协议实现工具,广泛应用于数据加密与解密、证书生成与管理以及其他安全性相关的任务。在现代网络安全中,OpenSSL 被用于构建和维护 SSL/TLS 通信,确保数据在传输过程中的机密性和完整性。 简单来…...
CSS 常见选择器
1. 基础选择器 元素选择器 选择所有指定类型的 HTML 元素。 p {color: blue; }选择所有 p 标签,并将文字颜色设为蓝色。 类选择器 选择带有特定类名的元素,类名前加 .。 .container {margin: 20px; }选择类名为 container 的所有元素。 ID 选择器 选…...
【Linux】shell脚本忽略错误继续执行
在 shell 脚本中,可以使用 set -e 命令来设置脚本在遇到错误时退出执行。如果你希望脚本忽略错误并继续执行,可以在脚本开头添加 set e 命令来取消该设置。 举例1 #!/bin/bash# 取消 set -e 的设置 set e# 执行命令,并忽略错误 rm somefile…...

python打卡day49
知识点回顾: 通道注意力模块复习空间注意力模块CBAM的定义 作业:尝试对今天的模型检查参数数目,并用tensorboard查看训练过程 import torch import torch.nn as nn# 定义通道注意力 class ChannelAttention(nn.Module):def __init__(self,…...

K8S认证|CKS题库+答案| 11. AppArmor
目录 11. AppArmor 免费获取并激活 CKA_v1.31_模拟系统 题目 开始操作: 1)、切换集群 2)、切换节点 3)、切换到 apparmor 的目录 4)、执行 apparmor 策略模块 5)、修改 pod 文件 6)、…...

工业安全零事故的智能守护者:一体化AI智能安防平台
前言: 通过AI视觉技术,为船厂提供全面的安全监控解决方案,涵盖交通违规检测、起重机轨道安全、非法入侵检测、盗窃防范、安全规范执行监控等多个方面,能够实现对应负责人反馈机制,并最终实现数据的统计报表。提升船厂…...

边缘计算医疗风险自查APP开发方案
核心目标:在便携设备(智能手表/家用检测仪)部署轻量化疾病预测模型,实现低延迟、隐私安全的实时健康风险评估。 一、技术架构设计 #mermaid-svg-iuNaeeLK2YoFKfao {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg…...

MFC内存泄露
1、泄露代码示例 void X::SetApplicationBtn() {CMFCRibbonApplicationButton* pBtn GetApplicationButton();// 获取 Ribbon Bar 指针// 创建自定义按钮CCustomRibbonAppButton* pCustomButton new CCustomRibbonAppButton();pCustomButton->SetImage(IDB_BITMAP_Jdp26)…...
java调用dll出现unsatisfiedLinkError以及JNA和JNI的区别
UnsatisfiedLinkError 在对接硬件设备中,我们会遇到使用 java 调用 dll文件 的情况,此时大概率出现UnsatisfiedLinkError链接错误,原因可能有如下几种 类名错误包名错误方法名参数错误使用 JNI 协议调用,结果 dll 未实现 JNI 协…...
c++ 面试题(1)-----深度优先搜索(DFS)实现
操作系统:ubuntu22.04 IDE:Visual Studio Code 编程语言:C11 题目描述 地上有一个 m 行 n 列的方格,从坐标 [0,0] 起始。一个机器人可以从某一格移动到上下左右四个格子,但不能进入行坐标和列坐标的数位之和大于 k 的格子。 例…...

《通信之道——从微积分到 5G》读书总结
第1章 绪 论 1.1 这是一本什么样的书 通信技术,说到底就是数学。 那些最基础、最本质的部分。 1.2 什么是通信 通信 发送方 接收方 承载信息的信号 解调出其中承载的信息 信息在发送方那里被加工成信号(调制) 把信息从信号中抽取出来&am…...
spring:实例工厂方法获取bean
spring处理使用静态工厂方法获取bean实例,也可以通过实例工厂方法获取bean实例。 实例工厂方法步骤如下: 定义实例工厂类(Java代码),定义实例工厂(xml),定义调用实例工厂ÿ…...