玩转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两个机器都获取锁成功了,都在执行业务操作,这种情况应该怎么处理?大家可以思考一下然后留言,我们一起讨论一下。

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

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

相关文章

【贪心的商人】python实现-附ChatGPT解析

1.题目 贪心的商人 知识点:贪心 时间限制:1s 空间限制: 256MB 限定语言:不限 题目描述: 商人经营一家店铺,有number种商品,由于仓库限制 每件商品的最大持有数量是item[index], 每种商品的价格在每天是item_price[item_index][day], 通过对商品的买进和卖出获取利润,请给…

去外地旅游遇到的问题和心得

去外地旅游核心的问题是&#xff1a;住、行、食三块&#xff0c;由于去一个陌生的城市&#xff0c;一切都不熟悉&#xff0c;也没有认识的人&#xff0c;所以一切都需要自己解决&#xff0c;而住、行、食就成最核心的问题&#xff0c;下面分别说明&#xff1a; 1 住&#xff0…

WordPress批量给没有图片的文章自动添加图片配图

每次写文章配图巨麻烦&#xff0c;特别是有些人批量采集文章&#xff0c;不可能一个个去配图&#xff0c;那么有没有什么方法能批量给WordPress没有图片的文章自动添加图片&#xff0c;并且还要自动识别该文章有没有图片&#xff0c;如果没有图片才自动配图&#xff0c;如果有图…

用于工业物联网和自动化的 Apache Kafka、KSQL 和 Apache PLC4

由于单一系统和专有协议&#xff0c;数据集成和处理是工业物联网&#xff08;IIoT&#xff0c;又名工业 4.0 或自动化工业&#xff09;中的巨大挑战。Apache Kafka、其生态系统&#xff08;Kafka Connect、KSQL&#xff09;和 Apache PLC4X 是以可扩展、可靠和灵活的方式实现端…

基础-MVP定位-找边算子

找边算子基于卡尺工具来检测边缘特征。 参数配置和应用&#xff1a; 期望的线段&#xff0c; 可以直接配置卡尺的起始点和终止点坐标&#xff0c;将卡尺移动到指定边上。 蓝色线为期望线&#xff0c;绿色线为找到的边。 找边模式&#xff1a;0/1 卡尺配置&#xff1a;设置 …

【文献阅读】Pocket2Mol : 基于3D蛋白质口袋的高效分子采样 + CrossDocked数据集说明

Pocket2Mol: Efficient Molecular Sampling Based on 3D Protein Pockets code&#xff1a; GitHub - pengxingang/Pocket2Mol: Pocket2Mol: Efficient Molecular Sampling Based on 3D Protein Pockets 所用数据集 与“A 3D Generative Model for Structure-Based Drug Desi…

(零)如何做机器视觉项目

文章目录 1 项目的前期准备1.1 从5个方面初步分析客户需求1.2 方案评估与验证1.3 签订合同 2 项目规划2.1 定义客户端的详细需求2.2 制定项目管理计划2.3 方案评审 3 详细设计3.1 硬件设备的选择与环境搭建3.2 软件开发平台与开发工具的选择3.3 机器视觉系统的整体框架与开发流…

MySQL进阶 —— 超详细操作演示!!!(下)

MySQL进阶 —— 超详细操作演示&#xff01;&#xff01;&#xff01;&#xff08;下&#xff09; 五、锁5.1 概述5.2 全局锁5.3 表级锁5.4 行级锁 六、InnoDB 引擎6.1 逻辑存储结构6.2 架构6.3 事务原理6.4 MVCC 七、MySQL 管理7.1 系统数据库7.2 常用工具 MySQL— 基础语法大…

使用代理IP进行安全高效的竞争情报收集,为企业赢得竞争优势

在激烈的市场竞争中&#xff0c;知己知彼方能百战百胜。竞争对手的信息对于企业来说至关重要&#xff0c;它提供了洞察竞争环境和市场的窗口。在这个信息时代&#xff0c;代理IP是一种实用的工具&#xff0c;可以帮助企业收集竞争对手的产品信息和营销活动数据&#xff0c;为企…

Linux UWB Stack实现——MCPS接口

本专栏主要介绍在Linux内核中的UWB Stack的实现&#xff0c;整体基于IEEE 802.15.4框架&#xff0c;采用Qorvo UWB芯片DW3000&#xff0c;开源协议版本为GPLv2。 在本篇文章中&#xff0c;主要介绍MCPS接口的定义&#xff0c;MCPS&#xff08;MAC Common Part Sublayer&#xf…

python二次开发CATIA:根据已知数据点创建曲线

已知数据点存于Coords.txt文件如下&#xff1a; 8.67155477658819,20.4471021292557,0 41.2016126836927,20.4471021292557,0 15.9568941320569,-2.93388599177698,0 42.2181532110364,-6.15301746150354,0 43.0652906622083,-26.4843096139083,0 -31.6617679595947,-131.1513…

分类预测 | MATLAB实现NGO-CNN北方苍鹰算法优化卷积神经网络数据分类预测

分类预测 | MATLAB实现NGO-CNN北方苍鹰算法优化卷积神经网络数据分类预测 目录 分类预测 | MATLAB实现NGO-CNN北方苍鹰算法优化卷积神经网络数据分类预测分类效果基本描述程序设计参考资料 分类效果 基本描述 1.Matlab实现NGO-CNN北方苍鹰算法优化卷积神经网络数据分类预测&…

Latex中公式输入

矩阵中的零 \mathrm{O}向量中零 \boldsymbol{0}单位矩阵 \mathrm{I}积分符号中dx的的用正体&#xff0c;x用斜体。 \mathrm {d} x数学模式下的正体字母&#xff1a; 在数学模式下&#xff0c;可以使用\mathrm{}命令将字母转换为正体。例如&#xff0c;\mathrm{A}将会显示为…

C++项目:【高并发内存池】

文章目录 一、项目介绍 二、什么是内存池 1.池化技术 2.内存池 3.内存池主要解决的问题 4.malloc 三、定长的内存池 四、高并发内存池整体框架设计 1.高并发内存池--thread cache 1.1申请内存&#xff1a; 1.2释放内存&#xff1a; 1.3用TLS实现thread cache无锁访…

rabbitMQ死信队列快速编写记录

文章目录 1.介绍1.1 什么是死信队列1.2 死信队列有什么用 2. 如何编码2.1 架构分析2.2 maven坐标2.3 工具类编写2.4 consumer1编写2.5 consumer2编写2.6 producer编写 3.整合springboot3.1 架构图3.2 maven坐标3.3 构建配置类&#xff0c;创建exchange&#xff0c;queue&#x…

想要精通算法和SQL的成长之路 - 二叉树的判断问题(子树判断 | 对称性 | 一致性判断)

想要精通算法和SQL的成长之路 - 二叉树的判断问题 前言一. 相同的树二. 对称二叉树三. 判断子树 前言 想要精通算法和SQL的成长之路 - 系列导航 一. 相同的树 原题链接 这题目典型的递归题&#xff1a; 如果两个节点都是null&#xff0c;我们返回true。如果两个节点一个nul…

Android 13.0 SystemUI下拉状态栏增加响铃功能

1.概述 在13.0的产品定制化开发中,在对systemui的状态栏开发中,对SystemUI下拉状态栏的QuickQSPanel区域有快捷功能键开关,对于增加各种响铃快捷也是常用功能, 有需要增加响铃功能开关功能,接下来就来分析SystemUI下拉状态栏QuickQSPanel快捷功能键开关的相关源码,然后实…

centos 部署nginx 并配置https

centos版本&#xff1a;centos 7.8 &#xff08;最好不要用8&#xff0c;8的很多用法和7相差很大&#xff09; 一.安装nginx 1。下载Nginx安装包&#xff1a;首先&#xff0c;访问Nginx的官方网站&#xff08;https://nginx.org/&#xff09;或您选择的镜像站点&#xff0c;找…

C#学生选课及成绩查询系统

一、项目背景 学生选课及成绩查询系统是一个学校不可缺少的部分&#xff0c;传统的人工管理档案的方式存在着很多的缺点&#xff0c;如&#xff1a;效率低、保密性差等&#xff0c;所以开发一套综合教务系统管理软件很有必要&#xff0c;它应该具有传统的手工管理所无法比拟的…

关于算法复杂度的几张表

算法在改进今天的计算机与古代的计算机的区别 去除冗余 数据点 算法复杂度 傅里叶变换