MySQL做分布式锁
分布式锁mysql实现方式
方式1:唯一索引
- 创建锁表,内部存在字段表示资源名及资源描述,同一资源名使用数据库唯一性限制。
- 多个进程同时往数据库锁表中写入对某个资源的占有记录,当某个进程成功写入时则表示其获取锁成功
- 其他进程由于资源字段唯一性限制插入失败陷入自旋并且失败重试。
- 当执行完业务后持有该锁的进程则删除该表内的记录,此时回到步骤一。

表数据
create table `database_lock`(`id` BIGINT NOT NULL AUTO_INCREMENT,`resource` INT NOT NULL COMMENT '锁资源',`description` varchar(1024) NOT NULL DEFAULT "" COMMENT '描述',PRIMARY KEY (`id`),UNIQUE KEY `resource` (`resource`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库分布式锁表';
db.properties
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/distribute_lock?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=Asia/Shanghai
user=root
password=123456
- PropertiesReader
@Slf4j
public class PropertiesReader {// Properties缓存文件private static final Map<String, Properties> propertiesCache = new HashMap<String, Properties>();public static Properties getProperties(String propertiesName) throws IOException {if (propertiesCache.containsKey(propertiesName)) {return propertiesCache.get(propertiesName);}loadProperties(propertiesName);return propertiesCache.get(propertiesName);}private synchronized static void loadProperties(String propertiesName) throws IOException {FileReader fileReader = null;try {// 创建Properties集合类Properties pro = new Properties();// 获取src路径下的文件--->ClassLoader类加载器ClassLoader classLoader = PropertiesReader.class.getClassLoader();URL resource = classLoader.getResource(propertiesName);// 获取配置路径String path = resource.getPath();// 读取文件fileReader = new FileReader(path);// 加载文件pro.load(fileReader);// 初始化propertiesCache.put(propertiesName, pro);} catch (IOException e) {log.error("读取Properties文件失败,Properties名为:" + propertiesName);throw e;} finally {try {if (fileReader != null) {fileReader.close();}} catch (IOException e) {log.error("fileReader关闭失败!", e);}}}
}
- JDBCUtils
@Slf4j
public class JDBCUtils {private static String url;private static String user;private static String password;static {//读取文件,获取值try {Properties properties = PropertiesReader.getProperties("db.properties");url = properties.getProperty("url");user = properties.getProperty("user");password = properties.getProperty("password");String driver = properties.getProperty("driver");//4.注册驱动Class.forName(driver);} catch (IOException | ClassNotFoundException e) {log.error("初始化jdbc连接失败!", e);}}/*** 获取连接* @return 连接对象*/public static Connection getConnection() throws SQLException {return DriverManager.getConnection(url, user, password);}/*** 释放资源* @param rs* @param st* @param conn*/public static void close(ResultSet rs, Statement st, Connection conn) {if (rs != null) {try {rs.close();} catch (SQLException e) {e.printStackTrace();}}if (st != null) {try {st.close();} catch (SQLException e) {e.printStackTrace();}}if (conn != null) {try {conn.close();} catch (SQLException e) {e.printStackTrace();}}}
}
数据库操作类
/*** MySQL 锁操作类(加锁+释放锁)*/
@Slf4j
public class MySQLDistributedLockService {private static Connection connection;private static Statement statement;private static ResultSet resultSet;static{try {connection = JDBCUtils.getConnection();statement = connection.createStatement();resultSet = null;} catch (SQLException e) {log.error("数据库连接失败!");}}/*** 锁表 - 获取锁* @param resource 资源* @param description 锁描述* @return 是否操作成功*/public static boolean tryLock(int resource,String description){String sql = "insert into database_lock (resource,description) values (" + resource + ", '" + description + "');";//获取数据库连接try {int stat = statement.executeUpdate(sql);return stat == 1;} catch (SQLException e) {return false;}}/*** 锁表-释放锁* @return*/public static boolean releaseLock(int resource) throws SQLException {String sql = "delete from database_lock where resource = " + resource;//获取数据库连接int stat = statement.executeUpdate(sql);return stat == 1;}/*** 关闭连接*/public static void close(){log.info("当前线程: " + ManagementFactory.getRuntimeMXBean().getName().split("@")[0] +",关闭了数据库连接!");JDBCUtils.close(resultSet,statement,connection);}
}
LockTable
/*** mysql分布式锁* 执行流程: 多进程抢占数据库某个资源,然后执行业务,执行完释放资源* 锁机制: 单一进程获取锁时,则其他进程提交失败*/
@Slf4j
public class LockTable extends Thread {@Overridepublic void run() {super.run();//获取Java虚拟机的进程IDString pid = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];try{while(true){log.info("当前进程PID:" + pid + ",尝试获取锁资源!");if(MySQLDistributedLockService.tryLock(1,"测试锁")){log.info("当前进程PID:" + pid + ",获取锁资源成功!");//sleep模拟业务处理过程log.info("开始处理业务!");Thread.sleep(10*1000);log.info("业务处理完成!");MySQLDistributedLockService.releaseLock(1);log.info("当前进程PID: " + pid + ",释放了锁资源!");break;}else{log.info("当前进程PID: " + pid + ",获取锁资源失败!");Thread.sleep(2000);}}}catch (Exception e){log.error("抢占锁发生错误!",e);}finally {MySQLDistributedLockService.close();}}// 程序入口public static void main(String[] args) {new LockTable().start();}
}
测试
运行时开启并行执行选项,每次运行三个或三个以上进程. Allow parallel run 运行并行执行


注意事项:
- 该锁为非阻塞的
- 当某进程持有锁并且挂死时候会造成资源一直不释放的情况,造成死锁,因此需要维护一个定时清理任务去清理持有过久的锁
- 要注意数据库的单点问题,最好设置备库,进一步提高可靠性
- 该锁为非可重入锁,如果要设置成可重入锁需要添加数据库字段记录持有该锁的设备信息以及加锁次数
方式二:基于乐观锁
- 每次执行业务前首先进行数据库查询,查询当前的需要修改的资源值(或版本号)。
- 进行资源的修改操作,并且修改前进行资源(或版本号)的比对操作,比较此时数据库中的值是否和上一步查询结果相同。
- 查询结果相同则修改对应资源值,不同则回到第一步。

例子:数据库中设定某商品基本信息(名为外科口罩,数量为10),多进程对该商品进行抢购,当商品数量为0时结束抢购。

代码实现
/*** 乐观锁-获取资源* @param id 资源ID* @return result*/public static ResultSet getGoodCount(int id) throws SQLException {String sql = "select * from database_lock_2 where id = " + id;//查询数据resultSet = statement.executeQuery(sql);return resultSet;}/*** 乐观锁-修改资源* @param id 资源ID* @param goodCount 资源* @return 修改状态*/public static boolean setGoodCount(int id, int goodCount) throws SQLException {String sql = "update database_lock_2 set good_count = good_count - 1 where id =" + id +" and good_count = " + goodCount;int stat = statement.executeUpdate(sql);return stat == 1;}/*** 乐观锁-开启事务自动提交*/public static void AutoCommit(){try {connection.setAutoCommit(true);} catch (SQLException e) {log.error("开启自动提交!",e);}}
OptimisticLock测试类
/*** mysql分布式锁-乐观锁* 执行流程: 多进程抢购同一商品,每次抢购成功商品数量-1,商品数据量为0时退出* 锁机制: 单一进程获取锁时,则其他进程提交失败*/
@Slf4j
public class OptimisticLock extends Thread{@Overridepublic void run() {super.run();String pid = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];ResultSet resultSet = null;String goodName = null;int goodCount = 0;try {while(true){log.info("当前线程:" + pid + ",开始抢购商品!");//获取当前商品信息resultSet = MySQLDistributedLockService.getGoodCount(1);while (resultSet.next()){goodName = resultSet.getString("good_name");goodCount = resultSet.getInt("good_count");}log.info("获取库存成功,当前商品名为:" + goodName + ",当前库存剩余量为:" + goodCount);//模拟执行业务操作Thread.sleep(2*3000);if(0 == goodCount){log.info("抢购失败,当前库存为0! ");break;}//修改库存信息,库存量-1if(MySQLDistributedLockService.setGoodCount(1,goodCount)){log.info("当前线程:" + pid + " 抢购商品:" + goodName + "成功,剩余库存为:" + (goodCount -1));//模拟延迟,防止锁每次被同一进程获取Thread.sleep(2 * 1000);}else{log.error("抢购商品:" + goodName +"失败,商品数量已被修改");}}}catch (Exception e){log.error("抢购商品发生错误!",e);}finally {if(resultSet != null){try {resultSet.close();} catch (SQLException e) {e.printStackTrace();log.error("关闭Result失败!" , e);}}MySQLDistributedLockService.close();}}public static void main(String[] args) {new OptimisticLock().start();}
}
代码测试
开启三个进程,查看执行情况



注意事项:
- 该锁为非阻塞的
- 该锁对于业务具有侵入式,如果设置版本号校验则增加的额外的字段,增加了数据库冗余
- 当并发量过高时会有大量请求访问数据库的某行记录,对数据库造成很大的写压力
- 因此乐观锁适用于并发量不高,并且写操作不频繁的场景
方式三:悲观锁实现方式(利用事务加上行/表锁)

实现思路
- 关闭jdbc连接自动commit属性
- 每次执行业务前先使用查询语句后接for update表示锁定该行数据(注意查询条件如果未命中主键或索引,此时将会从行锁变为表锁)
- 执行业务流程修改表资源
- 执行commit操作
代码实现
MySQLDistributedLockService
/*** 悲观锁-获取资源* @param id 资源ID* @return result*/public static ResultSet getGoodCount2(int id) throws SQLException {String sql = "select * from database_lock_2 where id = " + id + "for update";//查询数据resultSet = statement.executeQuery(sql);return resultSet;}/*** 悲观锁-修改资源* @param id 资源ID* @return 修改状态*/public static boolean setGoodCount2(int id) throws SQLException {String sql = "update database_lock_2 set good_count = good_count - 1 where id =" + id;int stat = statement.executeUpdate(sql);return stat == 1;}/*** 悲观锁-关闭事务自动提交*/public static void closeAutoCommit(){try {connection.setAutoCommit(false);} catch (SQLException e) {log.error("关闭自动提交失败!",e);}}/*** 悲观锁-提交事务*/public static void commit(String pid,String goodName,int goodCount) throws SQLException {connection.commit();log.info("当前线程:" + pid + "抢购商品: " + goodName + "成功,剩余库存为:" + (goodCount-1));}/*** 悲观锁-回滚*/public static void rollBack() throws SQLException {connection.rollback();}
PessimisticLock
/*** mysql 分布式锁-悲观锁* 执行流程:多个进程抢占同一个商品,执行业务完毕则通过connection.commit() 释放锁* 锁机制:单一进程获取锁时,则其他进程将阻塞等待*/
@Slf4j
public class PessimisticLock extends Thread {@Overridepublic void run() {super.run();ResultSet resultSet = null;String goodName = null;int goodCount = 0;String pid = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];//关闭自动提交MySQLDistributedLockService.closeAutoCommit();try{while(true){log.info("当前线程:" + pid + "");//获取库存resultSet = MySQLDistributedLockService.getGoodCount2(1);while (resultSet.next()) {goodName = resultSet.getString("good_name");goodCount = resultSet.getInt("good_count");}log.info("获取库存成功,当前商品名称为:" + goodName + ",当前库存剩余量为:" + goodCount);// 模拟执行业务事件Thread.sleep(2 * 1000);if (0 == goodCount) {log.info("抢购失败,当前库存为0!");break;}// 抢购商品if (MySQLDistributedLockService.setGoodCount2(1)) {// 模拟延时,防止锁每次被同一进程获取MySQLDistributedLockService.commit(pid, goodName, goodCount);Thread.sleep(2 * 1000);} else {log.error("抢购商品:" + goodName + "失败!");}}}catch (Exception e){//抢购失败log.error("抢购商品发生错误!",e);try {MySQLDistributedLockService.rollBack();} catch (SQLException ex) {log.error("回滚失败! ",e);}}finally {if(resultSet != null){try {resultSet.close();} catch (SQLException e) {log.error("Result关闭失败!",e);}}MySQLDistributedLockService.close();}}public static void main(String[] args) {new PessimisticLock().start();}
}
测试结果



注意事项:
- 该锁为阻塞锁
- 每次请求存在额外加锁的开销
- 在并发量很高的情况下会造成系统中存在大量阻塞的请求,影响系统的可用性
- 因此悲观锁适用于并发量不高,读操作不频繁的写场景
总结:
- 在实际使用中,由于受到性能以及稳定性约束,对于关系型数据库实现的分布式锁一般很少被用到。但是对于一些并发量不高、系统仅提供给内部人员使用的单一业务场景可以考虑使用关系型数据库分布式锁,因为其复杂度较低,可靠性也能够得到保证。
相关文章:
MySQL做分布式锁
分布式锁mysql实现方式 方式1:唯一索引 创建锁表,内部存在字段表示资源名及资源描述,同一资源名使用数据库唯一性限制。多个进程同时往数据库锁表中写入对某个资源的占有记录,当某个进程成功写入时则表示其获取锁成功其他进程由于…...
Python学习笔记:变量类型、字符串基本操作
1.注释 单行注释 # 单行注释 多行注释 """ 多行注释 """2.变量类型 # 基本变量类型 a 1 # integer b 1.5 # float c string # String d "string" # string e False # boolean # list\tuple\dictionar…...
JVM的组件、自动垃圾回收的工作原理、分代垃圾回收过程、可用的垃圾回收器类型
详细画的jvm模型图 https://www.processon.com/diagraming/64c8aa11c07d99075d934311 官方网址 https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html 相关概念 年轻代是所有新对象被分配和老化的地方。当年轻代填满时,这会导致m…...
【elementui】解决el-select组件失去焦点blur事件每次获取的是上一次选中值的问题
目录 【问题描述】 【问题摘要】 【分析问题】 【完整Test代码】 【封装自定义指令】 ↑↑↑↑↑↑↑↑↑↑↑↑ 不想看解决问题过程的可点击上方【封装自定义指令】目录直接跳转获取结果即可~~~ 【问题描述】 一位朋友遇到这么一个开发场景:在表格里面嵌入el-…...
通过了PMP考试,还有什么证书值得考?
自从7月24号公布了PMP成绩后,不少伙伴私信小编:通过PMP后还有哪些证书可以提升自己?一来是多份高含金量的证书可以多点竞争力,二来是加持自己的职业发展!今天小编就来给大家捋一捋! 一.NPDP认证 2016 年 4…...
页面技术基础-html
页面技术基础-html 环境准备:在JDBC中项目上完成代码定义 1. 新建一个 Module:filr->右键 -》Module -》Java-》next->名字(html_day1)->finish 2. 在 Moudle上右键-》第二个选项:add framework .. -> 选择JavaEE下第一个选项 Web Apllicat…...
/lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.28‘ not found
某项目中,我要给别人封装一个深度学习算法的SDK接口,运行在RK3588平台上,然后客户给我的交叉编译工具链是 然后我用他们给我的交叉编译工具链报下面的错误: aarch64-buildroot-linux-gnu-gcc --version /data/chw/aarch64/bin/cca…...
解决SVN或GIT忽略提交文件的问题
背景 使用IDEA 的SVN插件提交文件是总是会提交一些不需要提交的文件; 我们可以通过一些简单设置忽略这些文件。 git 在项目根目录新建文本文件,修改后缀为.gitignore 文件中添加内容 *.iml .project .gradle/ .idea/ target/ build/ .vscode/ .settings/ .facto…...
Django框架之路由用法
简介 路由简单的来说就是根据用户请求的 URL 链接来判断对应的处理程序,并返回处理结果,也就是 URL 与 Django 的视图建立映射关系。 Django 路由在 urls.py 配置,urls.py 中的每一条配置对应相应的处理方法。 Django 不同版本 urls.py 配…...
回文链表 LeetCode热题100
题目 给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。 思路 利用快慢指针找到链表中间节点,反转后半段链表。前半段从头节点开始与后半段比较。 注意当链表节点…...
如何在群晖NAS中使用cpolar内网穿透
如何在群晖nas中使用cpolar内网穿透 文章目录 如何在群晖nas中使用cpolar内网穿透 今天,我们来为大家介绍,如何在群晖系统中,使用图形化界面的cpolar。 cpolar经过图形化改造后,使用方法已经简便了很多,基本与其他应用…...
无头单向不循环链表和带头双向循环链表的创建
Lei宝啊:个人主页 愿所有美好不期而遇 前言: 接下来我们将会了解最基础的链表--->单链表 以及最方便也是最爽的链表--->带头双向循环链表。 若有看不懂之处,可画图或者借鉴这里:反转单链表,对于数据结构而言&am…...
超简单的fastapi链接websocket用例
main.py from typing import Listfrom fastapi import FastAPI, WebSocket, WebSocketDisconnectapp FastAPI()class ConnectionManager:def __init__(self):# 存放激活的ws连接对象self.active_connections: List[WebSocket] []async def connect(self, ws: WebSocket):# 等…...
MySQL详解
目录 一、MySQL 概述二、MySQL 安装和配置三、MySQL 基础语法四、MySQL 高级语法五、MySQL 性能优化六、MySQL 应用场景和实例七、MySQL 开发工具和插件八、MySQL 学习资源和社区 一、MySQL 概述 MySQL 是一种开源的关系型数据库管理系统,最初由瑞典的 MySQL AB 公…...
Vue [Day2]
指令修饰符 v-model.trim v-model.number 事件名.stop click.stop 事件名.prevent keyup.enter <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-w…...
【前端|Javascript第1篇】一文搞懂Javascript的基本语法
欢迎来到JavaScript的奇妙世界!作为前端开发的基石,JavaScript为网页增色不少,赋予了静态页面活力与交互性。如果你是一名前端小白,对编程一无所知,或者只是听说过JavaScript却从未涉足过,那么你来对了地方…...
【Linux命令200例】cp用于复制文件和目录(常用)
🏆作者简介,黑夜开发者,全栈领域新星创作者✌,阿里云社区专家博主,2023年6月csdn上海赛道top4。 🏆本文已收录于专栏:Linux命令大全。 🏆本专栏我们会通过具体的系统的命令讲解加上鲜…...
C高级_第二讲_shell指令和shell脚本_递归练习
思维导图 递归实现,输入一个数,输出这个数的每一位 int funh(int num){if(0 num){return 0;}else{funh(num/10);printf("%d\n", num%10);} }int main(int argc, const char *argv[]) {puts("请输入一个数");int num 0;scanf(&quo…...
静态路由综合实验
实验拓扑如下: 实验要求如下: 【1】R6为isp,接口IP地址均为公有地址;该设备只能配置IP地址,之后不能再对其进行任何配置 【2】R1~R5为局域网,私有IP地址192.168.1.0/24,请合理分配 【3】所有路由器上环回…...
Spring核心IOC控制反转思想-----Spring框架
import org.junit.Test;public class TestPublic {Testpublic void Test(){//控制反转是一种思想,是为了提高程序扩展力降低耦合度,达到DIP(Dependency Inversion Principle依赖倒置)原则//其核心是将对象的创建权交出去,由第三方容器负责管理,将对象和对象之间的维护权交出去,…...
Python|GIF 解析与构建(5):手搓截屏和帧率控制
目录 Python|GIF 解析与构建(5):手搓截屏和帧率控制 一、引言 二、技术实现:手搓截屏模块 2.1 核心原理 2.2 代码解析:ScreenshotData类 2.2.1 截图函数:capture_screen 三、技术实现&…...
RocketMQ延迟消息机制
两种延迟消息 RocketMQ中提供了两种延迟消息机制 指定固定的延迟级别 通过在Message中设定一个MessageDelayLevel参数,对应18个预设的延迟级别指定时间点的延迟级别 通过在Message中设定一个DeliverTimeMS指定一个Long类型表示的具体时间点。到了时间点后…...
Spark 之 入门讲解详细版(1)
1、简介 1.1 Spark简介 Spark是加州大学伯克利分校AMP实验室(Algorithms, Machines, and People Lab)开发通用内存并行计算框架。Spark在2013年6月进入Apache成为孵化项目,8个月后成为Apache顶级项目,速度之快足见过人之处&…...
工业安全零事故的智能守护者:一体化AI智能安防平台
前言: 通过AI视觉技术,为船厂提供全面的安全监控解决方案,涵盖交通违规检测、起重机轨道安全、非法入侵检测、盗窃防范、安全规范执行监控等多个方面,能够实现对应负责人反馈机制,并最终实现数据的统计报表。提升船厂…...
云启出海,智联未来|阿里云网络「企业出海」系列客户沙龙上海站圆满落地
借阿里云中企出海大会的东风,以**「云启出海,智联未来|打造安全可靠的出海云网络引擎」为主题的阿里云企业出海客户沙龙云网络&安全专场于5.28日下午在上海顺利举办,现场吸引了来自携程、小红书、米哈游、哔哩哔哩、波克城市、…...
Vue3 + Element Plus + TypeScript中el-transfer穿梭框组件使用详解及示例
使用详解 Element Plus 的 el-transfer 组件是一个强大的穿梭框组件,常用于在两个集合之间进行数据转移,如权限分配、数据选择等场景。下面我将详细介绍其用法并提供一个完整示例。 核心特性与用法 基本属性 v-model:绑定右侧列表的值&…...
条件运算符
C中的三目运算符(也称条件运算符,英文:ternary operator)是一种简洁的条件选择语句,语法如下: 条件表达式 ? 表达式1 : 表达式2• 如果“条件表达式”为true,则整个表达式的结果为“表达式1”…...
深入理解JavaScript设计模式之单例模式
目录 什么是单例模式为什么需要单例模式常见应用场景包括 单例模式实现透明单例模式实现不透明单例模式用代理实现单例模式javaScript中的单例模式使用命名空间使用闭包封装私有变量 惰性单例通用的惰性单例 结语 什么是单例模式 单例模式(Singleton Pattern&#…...
Spring AI 入门:Java 开发者的生成式 AI 实践之路
一、Spring AI 简介 在人工智能技术快速迭代的今天,Spring AI 作为 Spring 生态系统的新生力量,正在成为 Java 开发者拥抱生成式 AI 的最佳选择。该框架通过模块化设计实现了与主流 AI 服务(如 OpenAI、Anthropic)的无缝对接&…...
比较数据迁移后MySQL数据库和OceanBase数据仓库中的表
设计一个MySQL数据库和OceanBase数据仓库的表数据比较的详细程序流程,两张表是相同的结构,都有整型主键id字段,需要每次从数据库分批取得2000条数据,用于比较,比较操作的同时可以再取2000条数据,等上一次比较完成之后,开始比较,直到比较完所有的数据。比较操作需要比较…...
