基于 MySQL 排它锁实现分布式可重入锁解决方案

news2024/12/24 11:35:40

一、MySQL 排它锁和共享锁

在进行实验前,先来了解下MySQL 的排它锁和共享锁,在 MySQL 中的锁分为表锁和行锁,在行锁中锁又分成了排它锁和共享锁两种类型。

1. 排它锁

排他锁又称为写锁,简称X锁,是一种悲观锁,具有悲观锁的特征,如一个事务获取了一个数据行的X锁,其他事务尝试获取锁时就会等待另一个事务的释放。其中在 InnoDB 引擎下做写操作时 (UPDATE、DELETE、INSERT)都会自动给涉及到的数据加上 X 锁,因此当多线程情况下对同一条数据进行更新,在MySQL中不会出现线程安全问题。

其中 SELECT 语句默认不会加锁,如果查询的数据已经存在 X 锁,则会返回其最近提交的数据,如果希望每次获取的数据都是更新后最新的数据,当存在有更新时,则等待更新完成后获取新的值,这种情况下就需要对 SELECT 语句也要存在 X 锁,其中 SELECT 语句加 X 锁的话需要使用 FOR UPDATE 语句。

比如:当前有一张表结构如下:

CREATE TABLE `lock` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

写入一条测试数据:

INSERT INTO `testdb`.`lock`(`id`, `name`) VALUES (1, 'lock1');

下面,我使用 Navicat 开启了两个对话框,我在第一个对话框中,使用手动提交事务的方式执行更新语句,并且既不提交也不回滚事务:

BEGIN;
UPDATE `lock` SET `name` = 'lock2' WHERE id = 1; 

在这里插入图片描述
下面在另一个对话框中,查询 id = 1 的数据:

SELECT * FROM `lock` where id = 1

在这里插入图片描述
可以看到,并没有拿到最新的内容,因为此时 X 锁还没有释放,那此时对查询语句进行调整下,加上 FOR UPDATE 语句:

SELECT * FROM `lock` where id = 1 FOR UPDATE

在这里插入图片描述

此时会发现,查询语句一直在等待,因为这个查询语句在等待 X 锁的释放,下面对第一个对话框中,执行提交事务:

COMMIT;

在这里插入图片描述
在回到第二个对话框中查看:
在这里插入图片描述
已经拿到最新的值。这里需要注意下,你的是不是出现了超时报错,这是因为 Innodb 引擎对等待锁有个等待超时时间,默认情况下是 50s ,可以通过下面指令查看:

SHOW VARIABLES LIKE "Innodb_lock_wait_timeout"

在这里插入图片描述

如果感觉太小,可以通过下面指令调整:

SET innodb_lock_wait_timeout = 100

上面的操作已经感觉出来 X 锁的效果,那当两个 SELECT 语句都加上 FOR UPDATE 呢,比如在第一个回话框中,使用手动事务执行 SELECT 语句,同样不提交事务:

BEGIN;
SELECT * FROM `lock` where id = 1 FOR UPDATE;

在这里插入图片描述

在第二个对话框同样执行相同的代码,可以发现被阻塞掉了。

在这里插入图片描述

当第一个提交事务后,第二个紧接着也查出了信息,这也正符合排他锁的特征。

2. 共享锁

共享锁可以理解为读锁,简称S锁,可以对多个事务SELECT情况下读取同一数据时不会阻塞,但是如果存在写操作时 (UPDATE、DELETE、INSERT),SELECT语句也会被阻塞,在MySQL中使用 S 锁需要使用 LOCK IN SHARE MODE

例如还是开启两个对话框,在第两个对话框中,都查询 id = 1 的数据,并加上 S 锁,最后同样不提交事务:

BEGIN;
SELECT * FROM `lock` where id = 1 LOCK IN SHARE MODE;

在这里插入图片描述
可以发现两个都拿到了数据,对两个都提交事务后,假如第一个对话框中是更新操作,最后同样不提交事务:

BEGIN;
UPDATE `lock` SET `name` = 'lock3' WHERE id = 1 ;

在这里插入图片描述
在第二个对话框中还是加上 S 锁的查询操作:

BEGIN;
SELECT * FROM `lock` where id = 1 LOCK IN SHARE MODE;

在这里插入图片描述

可以看到查询被阻塞了,当第一个对话框中提交了事务,这里才会返回结果:

在这里插入图片描述
读到这里相信大家已经对 MySQL 的排它锁和共享锁有了一定的了解,下面我们基于 排它锁 实现分布式锁的场景。

二、基于 MySQL 排它锁实现分布式可重入锁

根据上面的实例可以看到排它锁具有阻塞等待的效果,和我们 JVM 中普通的锁的效果是一致的,但普通的锁通常只能在单个 JVM 中,但现在的服务,动则都要多台集群部署,对于不同的 JVM 普通的锁实在心有余而力不足,此时就要考虑使用分布式锁,目前分布式锁的解决方案也比较多,例如基于 RedissetNx 实现的分布式锁,相关框架有 Redissson ,还有基于 Zookeeper 的临时节点实现的分布式锁,相关框架有 Curator 等等,而且这些都有方案实现锁的可重入性。

本文我们再介绍一种基于 MySQL 的方案,毕竟现在再小的项目基本都会引入数据库,我们在此基础上延伸也少了其他框架的学习。

实现的思路:

  1. 数据库中创建一个lock 表 ,里面根据场景添加数据,一行就代表一个分布式锁的句柄。
  2. 在项目中在需要锁的方法中首先开启事务,保证下面的操作在事务中,事务可借助 Spring@Transactional 注解。
  3. 在获取锁时,使用 SELECT * FROM lock WHERE id = #{id} FOR UPDATE 排它锁语句执行。
  4. 如果正常查询到则获取锁成功,此时如果其他事务也在获取锁,则因为排他锁的原因会阻塞等待。
  5. 此时如果还要获取锁,也就是对于锁的可重入性设计,可以利用同一个事务中对于同一条数据 FOR UPDATE 不会阻塞的特征,只需在同一个事务中再次获取锁的操作即可实现 。
  6. 方法执行完,如果是手动事务一定要提交或回滚事务,即表示释放锁,如果是 Spring@Transactional 注解,则会自动提交或回滚。

开始实施:

首先新建一个 SpringBoot 项目,在 pom 中引入 mybatis-plus 依赖:

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.3.2</version>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.6</version>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

下面在配置文件中增加 MySQL 的配置:

server:
  port: 8081

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/testdb?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
    username: root
    password: root
    type: com.alibaba.druid.pool.DruidDataSource

下面获取锁的逻辑其实就是一个 Mapper 中的 Select 操作:

@Mapper
public interface LockMapper {

    /**
     * 尝试获取锁
     */
    @Select("SELECT id FROM `lock` where id = #{id} FOR UPDATE;")
    Long tryLock(@Param("id") Long id);
}

下面编写一个线程安全的例子,使用 10 个线程,去对一个全局 int 变量做 +1 操作,这里为了方便测试,直接声明成 Controller

@RestController
public class LockService {
	
	private volatile int count = 0;
	
    @GetMapping("/test")
    public void test() {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                testLock();
            }).start();
        }
    }

    public void testLock() {
        count++;
        System.out.print(count+" , ");
    }
}

运行后,访问测试接口,查看控制台打印的效果:
在这里插入图片描述
可以看到已经出现线程安全问题了,下面我们改造成使用 MySQL 的排他锁进行协调,这里需要注意下,这里事务使用的是 Spring@Transactional 注解,是基于 AOP 实现的,因此 LockService 需要从 Spring 容器中获取 ,另外对于锁的超时可以捕获 CannotAcquireLockException 异常。

@RestController
public class LockService {
    @Resource
    LockService lockService;

    @Resource
    LockMapper lockMapper;

    private final Long LOCK_ID = 1L;

    private volatile int count = 0;

    @GetMapping("/test")
    public void test() {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                lockService.testLock();
            }).start();
        }
    }

    @Transactional(rollbackFor = Exception.class)
    public void testLock() {
    	try {
	        //获取锁,如果获取不到则阻塞
	        if (Objects.nonNull(lockMapper.tryLock(LOCK_ID))){
	            count++;
	            System.out.print(count + " , ");
	        }
         } catch (CannotAcquireLockException e) {
            System.out.println("获取锁超时!");
        }
    }
}

执行后,查看日志:

在这里插入图片描述
细心地话可以明显感觉执行速度比之前慢了,因为出现了阻塞情况,通过数据可以看到已经解决了线程安全问题,但是锁的可重入性呢,我们在获取到锁后,再次获取锁看看是否正常,注意可重入锁表示锁中锁,锁的对象一定要是一致的,也就是这里的锁的 ID 要是一致的:

@RestController
public class LockService {

    @Resource
    LockService lockService;

    @Resource
    LockMapper lockMapper;

    private final Long LOCK_ID = 1L;

    private volatile int count = 0;

    @GetMapping("/test")
    public void test() {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                lockService.testLock();
            }).start();
        }
    }

    @Transactional(rollbackFor = Exception.class)
    public void testLock() {
    	try {
	        //获取锁,如果获取不到则阻塞
	        if (Objects.nonNull(lockMapper.tryLock(LOCK_ID))){
	            // 重入锁
	            if (Objects.nonNull(lockMapper.tryLock(LOCK_ID))){
	                count++;
	                System.out.print(count + " , ");
	            }
	        }
        } catch (CannotAcquireLockException e) {
            System.out.println("获取锁超时!");
        }
    }
}

运行后,查看日志:

在这里插入图片描述
可以看到可重入锁场景下也是可以正常获取到锁。

三、总结

本文基于 MySQL 实现的一种分布式可重入锁的效果,由于锁是使用的 MySQL 的排他锁,因此在多个 JVM 中也是可以实现锁的效果。这里主要讲解了实现思路,对于模块的封装没有做过多的设计,如果有想法的小伙伴也可以发动想法封装一下。另外由于是使用了 MySQL 如果是大量并发的情况下,可能会对 MySQL 造成一些压力。另外可能由于某些原因造成一端持有锁的时间过长,其余等待锁发生超时现象,超时情况这里未做处理,后续可以根据实际情况进行重试或错误处理。

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

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

相关文章

【C++】模板初阶STL简介

今天&#xff0c;你内卷了吗&#xff1f; 文章目录一、泛型编程二、函数模板&#xff08;显示实例化和隐式实例化&#xff09;1.函数模板格式2.单参数模板3.多参数模板4.模板参数的匹配原则三、类模板&#xff08;没有推演的时机&#xff0c;统一显示实例化&#xff09;1.类模…

RTOS之二环境搭建初识RTOS

参考&#xff1a;https://blog.csdn.net/kouxi1/article/details/123650688RTOS本质就是切换线程栈&#xff0c;栈换了环境就换了&#xff0c;一个重要的结构tcb&#xff08;linux叫PCB或thread_info&#xff09;&#xff1a;struct tcb{int32_t *sp; // 重要的sp指针&#xff…

seata【SAGA模式】代码实践(细节未必完全符合saga的配置,仅参考)

seata SAGA模式&#xff1a; 代码仍然是上一篇AT模式的代码&#xff1a;AT模式 不需要undo_log表 下面开始&#xff1a; 首先&#xff0c;saga模式依靠状态机的json文件来执行整个流程&#xff0c;其中的开始节点的服务即TM&#xff0c;然后状态机需要依靠三张表&#xff0…

【大数据】Hadoop-HA-Federation-3.3.1集群高可用联邦安装部署文档(建议收藏哦)

背景概述 单 NameNode 的架构使得 HDFS 在集群扩展性和性能上都有潜在的问题&#xff0c;当集群大到一定程度后&#xff0c;NameNode 进程使用的内存可能会达到上百 G&#xff0c;NameNode 成为了性能的瓶颈。因而提出了 namenode 水平扩展方案-- Federation。 Federation 中…

C语言---字符串函数总结

&#x1f680;write in front&#x1f680; &#x1f4dd;个人主页&#xff1a;认真写博客的夏目浅石. &#x1f381;欢迎各位→点赞&#x1f44d; 收藏⭐️ 留言&#x1f4dd; &#x1f4e3;系列专栏&#xff1a;夏目的C语言宝藏 &#x1f4ac;总结&#xff1a;希望你看完之…

ChatGPT国内使用方法全攻略(完整图文教程)

你好呀&#xff0c;我是月亮&#xff0c;一个90后的老程序员啦~ 最近ChatGPT完全火出圈了。 相关教程很多&#xff0c;我整理了一份网盘汇总&#xff0c;包含注册、谷歌浏览器插件使用、国内面注册平台&#xff0c;需要的小伙伴自取~ 网盘地址&#xff1a;使用方式汇总文档 …

数据库实践LAB大纲 06 INDEX

索引 索引是一个列表 —— 若干列集合和这些值的记录在数据表存储位置的物理地址 作用 加快检索速度唯一性索引 —— 保障数据唯一性加速表的连接分组和排序进行检索的时候 —— 减少时间消耗 一般建立原则 经常查询的数据主键外键连接字段排序字段少涉及、重复值多的字段…

分享114个JS菜单导航,总有一款适合您

分享114个JS菜单导航&#xff0c;总有一款适合您 114个JS菜单导航下载链接&#xff1a;https://pan.baidu.com/s/1t4_v0PipMjw3ULTLqkEiDQ?pwdgoi2 提取码&#xff1a;goi2 Python采集代码下载链接&#xff1a;https://wwgn.lanzoul.com/iKGwb0kye3wj $.ajax({type: &quo…

“万字“ Java I/O流讲解

Java I/O流讲解 每博一文案 谁让你读了这么多书&#xff0c;又知道了双水村以外还有一个大世界&#xff0c;如果从小你就在这个天地里&#xff0c;日出而作&#xff0c;日落而息。 那你现在就会和众乡亲抱同一理想&#xff1a;经过几年的辛劳&#xff0c;像大哥一样娶个满意的…

2023年中国各大城市薪酬报告出炉

全国地区&#xff1a;https://download.csdn.net/download/std86021/87322224北京&#xff1a;https://download.csdn.net/download/std86021/87273488上海&#xff1a;https://download.csdn.net/download/std86021/87322226广州&#xff1a;https://download.csdn.net/downlo…

Linux之文本搜索命令

文本搜索命令学习目标能够知道文本搜索使用的命令1. grep命令的使用命令说明grep文本搜索grep命令效果图:2. grep命令选项的使用命令选项说明-i忽略大小写-n显示匹配行号-v显示不包含匹配文本的所有行-i命令选项效果图:-n命令选项效果图:-v命令选项效果图:3. grep命令结合正则表…

linux基本功系列之hostname实战

文章目录前言一. hostname命令介绍二. 语法格式及常用选项三. 参考案例3.1 显示本机的主机名3.2 临时修改主机名3.3 显示短格式的主机名3.4 显示主机的ip地址四. 永久修改主机名4.1 centos6 修改主机名的方式4.2 centos7中修改主机名永久生效总结前言 大家好&#xff0c;又见面…

Java、JSP企业快信系统的设计与实现

技术&#xff1a;Java、JSP等摘要&#xff1a;计算机网络的出现到现在已经经历了翻天覆地的重大改变。因特网也从最早的供科学家交流心得的简单的文本浏览器发展成为了商务和信息的中心。到了今天&#xff0c;互联网已经成为了大量应用的首选平台&#xff0c;人们已经渐渐习惯了…

02- 天池工业蒸汽量项目实战 (项目二)

忽略警告: warnings.filterwarnings("ignore") import warnings warnings.filterwarnings("ignore") 读取文件格式: pd.read_csv(train_data_file, sep\t) # 注意sep 是 , , 还是\ttrain_data.info() # 查看是否存在空数据及数据类型train_data.desc…

线程池框架

这是之前有做的一个可以接受用户传入任意类型的任务函数和任意参数&#xff0c;并且能拿到任务对应返回值的一个线程池框架&#xff0c;可以链接成动态库&#xff0c;用在相关项目里面。一共实现了两版&#xff0c;都是支持fixed和cached模式的&#xff0c;半同步半异步的&…

全局向量的词嵌入(GloVe)

诸如词-词共现计数的全局语料库统计可以来解释跳元模型。 交叉熵损失可能不是衡量两种概率分布差异的好选择&#xff0c;特别是对于大型语料库。GloVe使用平方损失来拟合预先计算的全局语料库统计数据。 对于GloVe中的任意词&#xff0c;中心词向量和上下文词向量在数学上是等…

分享113个JS菜单导航,总有一款适合您

分享113个JS菜单导航&#xff0c;总有一款适合您 113个JS菜单导航下载链接&#xff1a;https://pan.baidu.com/s/1d4nnh-UAxNnSp9kfMBmPAw?pwdcw23 提取码&#xff1a;cw23 Python采集代码下载链接&#xff1a;https://wwgn.lanzoul.com/iKGwb0kye3wj base_url "http…

MySQL 4:MySQL函数

为了提高代码的复用性和隐藏实现细节&#xff0c;MySQL提供了很多函数。函数可以理解为别人封装好的模板代码。 在MySQL中&#xff0c;函数有很多&#xff0c;主要可以分为以下几类&#xff1a;聚合函数、数学函数、字符串函数、日期函数、控制流函数、窗口函数。 一、聚合函…

研一寒假C++复习笔记--深拷贝和浅拷贝代码实例

目录 1--深拷贝和浅拷贝的基础概念 2--浅拷贝的代码实例 3--深拷贝代码实例 4--参考 1--深拷贝和浅拷贝的基础概念 ① 浅拷贝&#xff1a;简单的赋值拷贝操作&#xff1b; ② 深拷贝&#xff1a;在堆区重新申请空间&#xff0c;进行拷贝操作&#xff1b; 2--浅拷贝的代码…

CUDA中的统一内存

文章目录1. Unified Memory Introduction1.1. System Requirements1.2. Simplifying GPU Programming1.3. Data Migration and Coherency1.4. GPU Memory Oversubscription1.5. Multi-GPU1.6. System Allocator1.7. Hardware Coherency1.8. Access Counters2. Programming Mode…