JPA与MySQL锁实战

news2025/1/15 23:42:21

前言:最近使用jpa和mysql时,遇到了死锁问题。在解决后将一些排查过程中新学到和复习到的知识点再总结整理一下。首先对InnoDB中锁相关的概念进行介绍,然后展示如何利用JPA提供的排他锁来实现想要的功能,最后对死锁问题进行讨论。

InnoDB锁的介绍

意向锁

意向锁是一个表级锁,一共有两种:意向共享锁和意向排他锁。主要的目的是表示当前表中某行记录正在被锁,或者即将被锁。事务在获取共享锁和排他锁之前,需要先获取对应的意向共享锁或者意向排他锁。

表级锁和行级锁是允许共存的,但也有不能共存的情况,比如当有一行记录存在排他锁时,就不允许再存在表锁了。

假设现在有一条记录被排他锁锁定,那么它会持有:该记录的行级排他锁,该表的意向排他锁。那么当另外一个事务想要将整张表锁定时,不需要挨个检查每个记录是否存在排他锁,只要检查该表是否有意向排他锁就可以达到目的了。

记录锁、间隙锁,临键锁

记录锁、间隙锁和临键锁是用来描述记录键锁的情况的。假设现在有如下几条记录:

1 3 5 7 9

每个记录之间是存在空间的,如1和3可以插入新的记录2。下面被括号包围的记录是被锁住的记录。

(1) 3( )5( 7) 9

记录锁

第一个括号包围的记录1,就是被记录锁锁住的。其他的事务不允许再更改1这条记录。记录锁实际上是锁住的索引,即便表里没有索引,InnoDB也会隐式创建一个聚簇索引来锁住。

间隙锁

第二个括号包围的是从3后面开始但不包括3,到5前面结束但不包括5的范围,锁住的是两条记录3和5之间的间隙,也就是间隙锁。

间隙锁和间隙锁不是互斥的,它的作用是保护两条记录的间隙不被插入新的记录。也即当在间隙锁锁住的范围进行插入操作时,需要进行等待。为什么间隙锁和间隙锁不互斥呢?

首先,前面说到间隙锁的作用是保护两条记录的间隙不被插入新记录,那么即便有两个间隙锁同时锁住了这个间隙,它们还是各自完成了自己的任务。

然后再考虑如下场景:

3( )5( )7

记录3和5之间被间隙锁锁住了,同时记录5和7之间的间隙也被锁住了,但记录5实际上是没有被任何锁锁住的。假设现在删除记录5:

3( )5( )7

那么这两个间隙锁必然要进行合并,锁住的内容就一样了:

3( )7

临键锁

上面第三个括号就是临键锁能锁住的范围,是记录5到记录7之间的间隙加上记录7本身。相当于是间隙锁在右面加上了一个记录锁。明白前面两个锁,这个自然也就明白了。

插入意向锁

插入意向锁不要和最开始提到的意向锁相混淆。插入意向锁使用的场景是:在对表进行insert操作之前,先要获取插入意向锁。插入意向锁来锁住要插入的记录两侧的间隙。比如当要在记录3和7之间插入记录5时,会锁住3到7之间的间隙:

3( 5 )7

可以看到插入意向锁实际上也是一种gap锁,不同事务的插入意向锁当然也不互相阻塞。

可重复读(Repeatable Read)

Mysql默认的事务隔离级别是可重复读,简称RR,RR隔离级别解决的并发事务下的幻读问题。复习一下什么是幻读:在一个事务中执行了两次查询,第二次查询结果中比第一次查询结果多出了记录,好像出现了幻觉。
假设现在表包含数据:0 1 2 4

事务1事务2
select * from test_table where id > 1 and id < 4 for update:2
insert into test_table values (3)
select * from test_table where id > 1 and id < 4 for update: 2

我们假设当前隔离级别是RC再来分析一下这个过程。首先在查询语句后跟了for update,无论结果怎样,我们的目的是不希望两次查询被干扰的,或者说两次查询的结果要是一样的。此时即便对记录1、2、4都加上锁,那么事务2执行的插入语句是能成功将3插入进来的,因为这是一条不存在的记录,仅凭记录锁是没有办法锁住的。但是如果在2和4之间的范围上加临键锁,那么此时事务2的插入就需要等待了,2和4之间的间隙能有效地被间隙锁保护,记录2也能被记录锁保护。这样引入了临键锁(间隙锁+记录锁)也就避免了幻读问题,使隔离级别升级到了RR。

另外说一下,Oracle和PostgreSQL的默认事务隔离级别都是RC。

JPA排他锁

在介绍jpa之前,先说一下sql语句select ... for update,使用for update的前提是手动管理事务,即通过start transaction开启事务后再查询。for update的加锁周期是从事务开始到事务结束或回滚。for update的加锁有两种情况:

  1. where条件不是索引
    这种情况下会直接将整个表锁住。
  2. where条件上有索引
    有索引时会将符合条件的索引都锁住。

现在给出一个场景:有一张表test_tb,包括两个字段,idstatus。两个字段均建有索引。每次接到一个请求,表中会插入statsu2的数据。需求是,每经过一段时间,将最先插入的status2的数据,更新为status = 1。服务是多实例部署,因此在读取数据时一定会考虑使用for update

@Lock注解

在JPA中,使用for update语句只需要在Repository接口方法上添加注解@Lock(LockModeType.PESSIMISTIC_WRITE),比如下面这个方法:

@Lock(LockModeType.PESSIMISTIC_WRITE)
TestTb findFirstByStatus(int status);

当然,仅仅标记一个@Lock注解是不够的。我们前面提到了,for update语句是需要在事务中执行的,因此还必须与事务注解搭配使用。

插入语句是比较简单的,看一下使用sql如何达到更新的目的:

start;
select * from test_tb t where t.status = 2 limit 1 for update;
update test_tb t set t.status = 1 where t.id = 1001; -- 这里id应该是select查出来的
commit;

如何使用JPA将上面的sql转为代码呢?实际上很简单,除了上面的接口方法外,还需要在另外一个类中新建一个方法:

	@Transactional
    public TestTb findTestTbByStatusOnLock(int status) {
        TestTb testTb = testTbRepository.findFirstByStatus(status);
        // 不存在是返回null
        if (testTb == null) {
            return null;
        }
		
		// 更新状态并保存至数据库
        testTb.setStatus(1);
        testTbRepository.save(testTb);

        return testTb;
    }

多实例读取缓存问题

在上面的方法中,实现了查询和更新的完整事务。不过这样写还是存在问题的,表现出来的现象就是没有锁住。有同学可能会想,是不是因为调用的save方法没有及时flush。我们知道save方法在执行后并不会立即保存到数据库,而是会先被缓存起来,必须要进行一个flush操作后才会立即同步到数据库。实际上出问题的地方并不在保存这一步,在事务提交时数据就会写入数据库了。原因在于前面testTbRepository.findFirstByStatus(status)这一步读取到的很可能并不是数据库中最新的数据(缓存中的数据),从而导致前面的findTestTbByStatusOnLock方法会重复更新其他实例已经读取并更新过的内容。

同时这里还会产生死锁,在死锁的时候会产生异常使该方法不能返回值 。因此还需要对异常和事务回滚做一下处理:

	// Exception用于捕获死锁异常
	@Transactional(rollbackFor = Exception.class)
    public TestTb findTestTbByStatusOnLock(int status) {
        TestTb testTb;
        try {
            testTb = testTbRepository.findFirstByStatus(status);
        } catch (Exception e) {
            // 死锁异常时手动回滚,这样方法才能有返回值
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            return null;
        }

        testTb.setStatus(1);
        testTbRepository.save(testTb);

        return testTb;
    }

我们先来解决没有锁住,也就是重复更新问题。其实很简单,我们不用缓存就可以了。并且在这种情况下,我们是希望每次查询都去数据库读取最新状态的,没有使用缓存的必要。因此我们在实体类上通过注解@Cacheable关闭就可以了:

@Entity
@Table(name = "test_tb")
@Cacheable(value = false)
public class TestTb {
	// ···
}

死锁

前面实际上已经实现了场景所需的功能,只是有可能会报出死锁的错误。因此将死锁问题单独拿出来分析。首先对场景进行复现:
在这里插入图片描述

begin;
SELECT * FROM test_DB.test_tb tt WHERE tt.status = 2 limit 1 for UPDATE;   --1
UPDATE test_DB.test_tb tt SET tt.status = 1 WHERE tt.id = 3;			   --2
commit;

上面sql分别在两个会话中执行,会话1执行1,会话2执行1,会话1执行2,此时会话2产生死锁。

死锁日志及分析

再来看一下会产生的死锁日志show engine innodb status;

------------------------
LATEST DETECTED DEADLOCK
------------------------
2023-12-04 15:42:59 139984137955072
*** (1) TRANSACTION:
TRANSACTION 1961, ACTIVE 39 sec starting index read	// 根据索引读取数据
mysql tables in use 1, locked 1	// 锁住一张表,一行数据
LOCK WAIT 2 lock struct(s), heap size 1128, 1 row lock(s)	// 等待2个锁结构
MySQL thread id 47, OS thread handle 139984461809408, query id 923 192.168.1.3 root executing
/* ApplicationName=DBeaver 23.2.5 - SQLEditor <Console> */ SELECT * FROM test_DB.test_tb tt WHERE tt.status = 2 limit 1 for UPDATE // 执行的sql语句

*** (1) HOLDS THE LOCK(S):	// 持有的锁																							// 写锁正在等待,应该是临键锁
RECORD LOCKS space id 2 page no 5 n bits 80 index test_tb_status_IDX of table `test_DB`.`test_tb` trx id 1961 lock_mode X waiting	
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 32
// 锁的索引上的信息
 0: len 4; hex 80000002; asc     ;;	// hex 16进制编码,status 2
 1: len 4; hex 80000003; asc     ;;    // id 3


*** (1) WAITING FOR THIS LOCK TO BE GRANTED:	// 正在等待的锁
RECORD LOCKS space id 2 page no 5 n bits 80 index test_tb_status_IDX of table `test_DB`.`test_tb` trx id 1961 lock_mode X waiting
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 32
 0: len 4; hex 80000002; asc     ;;
 1: len 4; hex 80000003; asc     ;;


*** (2) TRANSACTION:
TRANSACTION 1960, ACTIVE 55 sec updating or deleting	// 执行更新或删除操作
mysql tables in use 1, locked 1
LOCK WAIT 4 lock struct(s), heap size 1128, 3 row lock(s), undo log entries 1
MySQL thread id 45, OS thread handle 139984467093248, query id 930 192.168.1.3 root updating		
/* ApplicationName=DBeaver 23.2.5 - SQLEditor <Console> */ UPDATE test_DB.test_tb tt SET tt.status = 1 WHERE tt.id = 3 // 更新语句导致的死锁

*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 2 page no 5 n bits 80 index test_tb_status_IDX of table `test_DB`.`test_tb` trx id 1960 lock_mode X	// 持有写锁,临键锁
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 32
 0: len 4; hex 80000002; asc     ;;
 1: len 4; hex 80000003; asc     ;;


*** (2) WAITING FOR THIS LOCK TO BE GRANTED:																											// 等待插入意向锁
RECORD LOCKS space id 2 page no 5 n bits 80 index test_tb_status_IDX of table `test_DB`.`test_tb` trx id 1960 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 32
 0: len 4; hex 80000002; asc     ;;
 1: len 4; hex 80000003; asc     ;;

*** WE ROLL BACK TRANSACTION (1)

事务一在查询时申请了status索引上记录2的临键锁,和id索引上记录3的锁:
在这里插入图片描述

	1		1(  	   2	  		2) 	    2	
 	1		2   	  (3)		    4		5

接着事务二在查询时同样申请到status索引上记录2的临建锁,等待id索引上记录1的锁:
在这里插入图片描述

	1		1([  	   2	  		2)] 	    2	
 	1		2   	  (3)		    4		    5

事务一执行更新语句获取status上记录2的插入意向锁,导致和事务二死锁:
在这里插入图片描述

	1		1{([  	   2	  		2)] 	    2	}
 	1		2   	  (3)		    4		    5

解决方案

解决的方案就是将数据库隔离级别进行降级,SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;设置数据库隔离级别为RC。

在我们这个例子中,对查询方法执行会话级别的设置也是可以的:@Transactional(rollbackFor = Exception.class, isolation = Isolation.READ_COMMITTED)。此外,针对这种类似队列的场景,使用skip locked语句也能较好地处理。该语句的作用是跳过被锁定的记录进行读取。

RR隔离级别并非一定优于RC级别,在并发量较大时使用RC级别能更好地保证数据库性能。

文中只是对一个死锁场景进行了分析,但在分析过程中也查阅了相关资料。会产生死锁的情况非常多,可以参考一下:https://github.com/aneasystone/mysql-deadlocks/。

解决死锁问题的方法通常也是关注以下几点:

  • 打印和分析相关日志,包括数据库日志和应用日志
  • 尽量缩短事务范围,减少事务间的业务代码
  • 事务持续时间不宜过长
  • 使用for update或for share时降低隔离级别
  • 事务间操作按顺序执行,避免交叉

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

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

相关文章

【EI会议征稿】第三届密码学、网络安全和通信技术国际会议(CNSCT 2024)

第三届密码学、网络安全和通信技术国际会议&#xff08;CNSCT 2024&#xff09; 2024 3rd International Conference on Cryptography, Network Security and Communication Technology 随着互联网和网络应用的不断发展&#xff0c;网络安全在计算机科学中的地位越来越重要&…

solidity案例详解(六)服务评价合约

有服务提供商和用户两类实体&#xff0c;其中服务提供商部署合约&#xff0c;默认诚信为true&#xff0c;用户负责使用智能合约接受服务及评价&#xff0c;服务提供商的评价信息存储在一个映射中&#xff0c;可以根据服务提 供商的地址来查找评价信息。用户评价信息&#xff0c…

Linux学习笔记2

web服务器部署&#xff1a; 1.装包&#xff1a; [rootlocalhost ~]# yum -y install httpd 2.配置一个首页&#xff1a; [rootlocalhost ~]# echo i love yy > /var/www/html/index.html 启动服务&#xff1a;[rootlocalhost ~]# systemctl start httpd Ctrl W以空格为界…

定时器的使用及实现

在Java中&#xff0c;定时器&#xff08;Timer&#xff09;是一个用于执行任务的工具类。它可以安排任务在指定的时间点执行&#xff0c;或者按照指定的时间间隔周期性地执行。 1. Timer类 Timer类位于java.util包中&#xff0c;它提供了一种简单而便利的方式来安排以后的任务…

1.4 场景设计精要

一、场景主题确定 设计游戏场景首先明确游戏发生的时间地点等时代背景。通过对玩家动线的设计&#xff0c;功能模型的合理布局构建出场景的基本骨架。利用光影效果和色彩变化烘托场景氛围。 市场上常见的主题场景&#xff1a;剑侠、科幻、废墟、魔幻等 二、场景风格确定 大类分…

深入理解mysql的explain命令

1 基础 全网最全 | MySQL EXPLAIN 完全解读 1.1 MySQL中EXPLAIN命令提供的字段包括&#xff1a; id&#xff1a;查询的标识符。select_type&#xff1a;查询的类型&#xff08;如SIMPLE, PRIMARY, SUBQUERY等&#xff09;。table&#xff1a;查询的是哪个表。partitions&…

Vue学习计划-Vue2--Vue核心(七)生命周期

抛出问题&#xff1a;一进入页面就开启一个定时器&#xff0c;每隔1秒count就加1&#xff0c;如何实现 示例&#xff1a; <body> <div id"app">{{ n }}<button click"add">执行</button> </div><script>let vm new …

西工大计算机学院计算机系统基础实验一(函数编写15~17)

还是那句话&#xff0c;稳住心态&#xff0c;稳住心态&#xff0c;稳住心态。心里别慌&#xff0c;心里别慌&#xff0c;心里别慌。 第15题&#xff0c;howManyBits&#xff0c;返回用二进制补码形式表示x所需的最小二进制位数。比如howManyBits(12) 5&#xff0c;12可以被表…

高级实现Java的七大热门技术框架解析源码特性分析

Java是一门广泛应用的编程语言&#xff0c;拥有众多热门技术框架。本文将通过解析源码和特性分析&#xff0c;带你深入了解Java的七大热门技术框架&#xff0c;并提供相关示例代码。 一、Spring框架 Spring是Java最流行的开发框架之一&#xff0c;提供了依赖注入&#xff08;D…

电商早报 | 12月7日| 阿里巴巴分红179亿,破历史记录

阿里巴巴将派发25亿美元年度股息 12月6日消息&#xff0c;阿里巴巴发布公告&#xff0c;将向截至2023年12月21日香港时间及纽约时间收市时登记在册的普通股持有人和美国存托股持有人&#xff0c;就2023财年首次派发年度股息&#xff0c;金额分别为每股普通股0.125美元或每股美…

mysql知识分享(包含安装卸载)(一)

如果博客有错误&#xff0c;请佬指正。 目录 注意&#xff1a;打开cmd时要有管理员身份打开&#xff0c;重要 为何使用数据库&#xff1f; 数据库的相关概念 关系型数据库 关系型数据库设计规则 表&#xff0c;记录&#xff0c;字段 表的关联关系 一对一关联 一对多关系 …

如何衡量和提高测试覆盖率?

衡量和提高测试覆盖率&#xff0c;对于尽早发现软件缺陷、提高软件质量和用户满意度&#xff0c;都具有重要意义。如果测试覆盖率低&#xff0c;意味着用例未覆盖到产品的所有代码路径和场景&#xff0c;这可能导致未及时发现潜在缺陷&#xff0c;代码中可能存在逻辑错误、边界…

[Geek Challenge 2023] web题解

文章目录 EzHttpunsignn00b_Uploadeasy_phpEzRceezpythonezrfi EzHttp 按照提示POST传参 发现密码错误 F12找到hint&#xff0c;提示./robots.txt 访问一下&#xff0c;得到密码 然后就是http请求的基础知识 抓包修改 最后就是 我们直接添加请求头O2TAKUXX: GiveMeFlag 得到…

vue中的动画组件使用及如何在vue中使用animate.css

“< Transition >” 是一个内置组件&#xff0c;这意味着它在任意别的组件中都可以被使用&#xff0c;无需注册。它可以将进入和离开动画应用到通过默认插槽传递给它的元素或组件上。进入或离开可以由以下的条件之一触发&#xff1a; 由 v-if 所触发的切换由 v-show 所触…

用 C 写一个卷积神经网络

用 C 写一个卷积神经网络 深度学习领域最近发展很快&#xff0c;前一段时间读transformer论文《Attention Is All You Need》时&#xff0c;被一些神经网络和深度学习的概念搞得云里雾里&#xff0c;其实也根本没读懂。发现深度学习和传统的软件开发工程领域的差别挺大&#xf…

数据结构:图文详解双向链表的各种操作(头插法,尾插法,任意位置插入,查询节点,删除节点,求链表的长度... ...)

目录 一.双向链表的概念 二.双向链表的数据结构 三.双向链表的实现 节点的插入 头插法 尾插法 任意位置插入 节点的删除 删除链表中第一次出现的目标节点 删除链表中所有与关键字相同的节点 节点的查找 链表的清空 链表的长度 四.模拟实现链表的完整代码 前言&am…

多人群聊代码

服务端 import java.io.*; import java.net.*; import java.util.ArrayList; public class Server{public static ServerSocket server_socket;public static ArrayList<Socket> socketListnew ArrayList<Socket>(); public static void main(String []args){try{…

5G - NR物理层解决方案支持6G非地面网络中的高移动性

文章目录 非地面网络场景链路仿真参数实验仿真结果 非地面网络场景 链路仿真参数 实验仿真结果 Figure 5 && Figure 6&#xff1a;不同信噪比下的BER和吞吐量 变量 SISO 2x2MIMO 2x4MIMO 2x8MIMOReyleigh衰落、Rician衰落、多径TDL-A(NLOS) 、TDL-E(LOS)(a)QPSK (b)16…

echarts环形饼图

效果示例 代码汇总 pieCharts() {let data [];const providerResult [{name: 智诺, value: 23},{name: 海康, value: 5},{name: 大华, value: 5}, {name: 云科, value: 23},{name: 四信, value: 22},{name: 九物, value: 22}]let charts echarts.init(document.getElemen…

700G全球30米高程DEM原始数据

这里&#xff0c;为大家分享700G的全球30米高程原始数据。 全球30米高程覆盖范围 NASA全球30米SRTM高程DEM数据范围在南纬56度到北纬61度范围之间&#xff0c;共分为14520个区域范围。 每个区域范围在经纬度方向的跨度均为1度大小&#xff0c;将该接图表在微图中与影像叠加之…