玩转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.xdnf.cn/news/146986.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

此芯科技加入百度飞桨硬件生态共创计划,加速端侧AI生态布局

近日&#xff0c;此芯科技&#xff08;上海&#xff09;有限公司&#xff08;以下简称“此芯科技”&#xff09;与百度签署硬件生态共创计划合作协议&#xff0c;正式加入由百度发起的硬件生态共创计划。双方将共同推动端侧AI和大模型在个人计算、车载计算以及元宇宙计算等领域…

拒绝水文!八大排序(二)【适合初学者】冒泡排序和选择排序

文章目录 冒泡排序选择排序 大家好&#xff0c;我是纪宁。 这篇文章介绍八大排序中思路最简单&#xff0c;但效率也是最低的两种排序算法&#xff01; 冒泡排序 冒泡排序&#xff0c;可以说是每个人在接触编程时最先学会的一种排序。 冒泡排序基本思想 冒泡排序&#xff08;…

Rust 围炉札记

文章目录 一、安装二、文档 一、安装 rust 安装 卸载&#xff08;自定义路径&#xff09; Rust配置镜像源 Rust in Visual Studio Code Rust 官网 Rust 中文网 windows系统下Rust环境搭建以及vscode调试环境配置 二、文档 Rust 菜鸟教程 Rust 官方文档中文教程 Rust 包管理…

Docker从认识到实践再到底层原理(七)|Docker存储卷

前言 那么这里博主先安利一些干货满满的专栏了&#xff01; 首先是博主的高质量博客的汇总&#xff0c;这个专栏里面的博客&#xff0c;都是博主最最用心写的一部分&#xff0c;干货满满&#xff0c;希望对大家有帮助。 高质量博客汇总 然后就是博主最近最花时间的一个专栏…

【1++的Linux】之进程(五)

&#x1f44d;作者主页&#xff1a;进击的1 &#x1f929; 专栏链接&#xff1a;【1的Linux】 文章目录 一&#xff0c;什么是进程替换二&#xff0c;替换函数三&#xff0c;实现我们自己的shell 一&#xff0c;什么是进程替换 我们创建出来进程是要其做事情的&#xff0c;它可…

【爬虫】用wget命令爬虫的简易教程

文章目录 1. 获取登录的请求2. 用postman模拟登录请求3. 用wget模拟登录请求并保存cookie4. 开始爬取网站5. 查看爬取结果6. 网站爬虫简易教程 爬取需要登录的网站的资源 背景&#xff1a;对于一些网站需要使用用户名和密码登录并且使用了https&#xff0c;我们如果不通过凭证将…

网络工程师对口的岗位是什么

大家好&#xff0c;我是网络工程师成长日记实验室的郑老师&#xff0c;您现在正在查看的是网络工程师成长日记专栏&#xff0c;记录网络工程师日常生活的点点滴滴 网络工程师可以从事哪些职位呢&#xff1f;其实网络工程师最对应的最对口的这个岗位就是高级网络工程师、系统集成…

番外6:下载+安装+配置Linux

#########配置Linux---后续 step08: 点击编辑虚拟机设置&#xff0c;选择下载好的映像文件.iso进行挂载&#xff1b; step09: 点击编辑虚拟机选项&#xff0c;选择UEFI启动模式并点击确定&#xff1b; step10: 点击开启虚拟机&#xff0c;选择Install rhel &#xff1b; 备注&…

飞桨EasyDL-Mac本地部署离线SDK-Linux集成Python

前言&#xff1a;本文对使用飞桨EasyDL桌面版实现本地部署物体检测做一下说明 一、训练模型 如何使用飞桨EasyDL桌面版这里就不再赘述&#xff0c;直接参照官方文档进行物体检测模型训练。 飞桨EasyDL桌面版-用零代码开发实现物体检测https://ai.baidu.com/ai-doc/EASYDL/Tl2…

探索社会工程的深度:从定义到高级攻击策略

在广阔的网络安全领域&#xff0c;社会工程作为一种微妙的威胁而出现&#xff0c;它利用人类的漏洞来访问敏感信息或实施欺诈。网络安全背景下的社会工程的定义很明确&#xff1a;它包括使用欺骗手段操纵个人泄露机密或个人信息&#xff0c;然后将这些信息用于欺诈目的。 此类…

实现水平垂直居中的十种方式

本文节选自我的博客&#xff1a;实现水平垂直居中的十种方式 &#x1f496; 作者简介&#xff1a;大家好&#xff0c;我是MilesChen&#xff0c;偏前端的全栈开发者。&#x1f4dd; CSDN主页&#xff1a;爱吃糖的猫&#x1f525;&#x1f4e3; 我的博客&#xff1a;爱吃糖的猫&…

AJAX--Express速成

一、基本概念 1、AJAX(Asynchronous JavaScript And XML)&#xff0c;即为异步的JavaScript 和 XML。 2、异步的JavaScript 它可以异步地向服务器发送请求&#xff0c;在等待响应的过程中&#xff0c;不会阻塞当前页面。浏览器可以做自己的事情。直到成功获取响应后&#xf…

Scala第十五章节

Scala第十五章节 1. 递归 2. 案例一: 求阶乘 3. 案例二: 斐波那契数列 4. 案例三: 打印目录文件 scala总目录 文档资料下载

淘宝天猫复制商品链接粘贴到草柴查优惠券iPhone苹果手机粘贴弹窗怎么关闭?

经常在淘宝、天猫、京东网购&#xff0c;挑选商品后复制链接&#xff0c;到草柴APP查询要购买商品的优惠券和返利&#xff0c;iPhone苹果手机每次粘贴复制的商品链接都弹窗提示特别烦人。接下来分享如何关闭草柴APP复制粘贴提醒的弹窗&#xff1b; 如何永久关闭iPhone苹果手机复…

云服务器租用价格表概览_阿里云腾讯云华为云

云服务器租用价格多少钱一年&#xff1f;阿腾云分享阿里云、腾讯云和华为云的云服务器租用价格表&#xff1a;阿里云2核2G服务器108元一年起、腾讯云2核2G3M带宽轻量服务器95元一年、华为云2核2G3M云耀L实例89元一年起&#xff0c;阿腾云分享更多关于云服务器租用价格明细&…

前端JavaScript入门到精通,javascript核心进阶ES6语法、API、js高级等基础知识和实战 —— Web APIs(四)

思维导图 一、日期对象 1.1 实例化 实例化&#xff0c;默认得到当前时间&#xff0c;也可以指定时间 1.2 日期对象方法 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible&q…

[Realtek sdk-3.4.14b]RTL8197FH-VG 2.4G to WAN吞吐量低于60%的问题分析及解决方案

问题描述 RTL8197FH-VG 2.4G wifi to WAN吞吐量低于65%的标准,正常2T2R的wifi 300Mbps x 65% = 195Mbps,但是实际只能跑到160Mbps,这个时候CPU的idl已经为0,sirq占用率达到98%左右 网络拓扑 一台PC通过2.4G WiFi连接到RTL8197FH-VG,另外一台PC直接通过WAN口连接到RTL8197…

milvus 结合Thowee 文本转向量 ,新建表,存储,搜索,删除

1.向量数据库科普 【上集】向量数据库技术鉴赏 【下集】向量数据库技术鉴赏 milvus连接 from pymilvus import connections, FieldSchema, CollectionSchema, DataType, Collection, utility connections.connect(host124.****, port19530)2.milvus Thowee 文本转向量 使用 …

web:[RoarCTF 2019]Easy Calc

题目 进入页面是一个计算器的页面 随便试了一下 查看源代码看看有什么有用的信息 访问一下这个calc.php 进行代码审计 <?php error_reporting(0); if(!isset($_GET[num])){show_source(__FILE__); }else{$str $_GET[num];$blacklist [ , \t, \r, \n,\, ", , \[, \]…

C语言编译与链接过程详解

C语言编译与链接过程详解 源文件 main.c #include <stdio.h>extern int data; extern int add(int a,int b);int a1; int a2 0; int a3 10;static int b1; static int b2 0; static int b3 20;int main() {int c1;int c2 0;int c3 30;static int d1;static int …