视频地址: https://www.bilibili.com/video/bv1zY411N7tB
最近在调试接口的时候遇到了MySQL死锁问题,我自己测试的时候一切都好好的,但在并发下,就死锁了
其实死锁问题,并没有一个类似“万金油”的解决办法,解铃还需系铃人,能做的只有写这个代码的人自己去解决
第一次出现死锁,想必你也会和我一样整个人都是懵的,不知道如何下手,等你看过下面的文章明白了死锁,就不会害怕了
死锁异常
2022-06-15 16:05:22.216 ERROR 38907 --- [ XNIO-1 task-4] c.i.c.exception.GlobalExceptionHandler : ### Error updating database. Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction### The error may exist in file [/Users/xiaodaoxian/Desktop/work/code/ideamake-market-cloud-third-dock/target/classes/mapper/YxxWkBatchDataMapper.xml]### The error may involve defaultParameterMap### The error occurred while setting parameters### SQL: DELETE FROM xdx_test_two WHERE user_id = ?### Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction; Deadlock found when trying to get lock; try restarting transaction; nested exception is com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
一、理论(重要)
所谓的死锁,一定是两个线程互相等待对方的资源。对于MySQL 来说,那就是两个事物
锁住了对方的数据。
MySQL的innodb有 表锁、行锁,如果锁表那死锁的概率很大,一般我们操作都是带有参数的 where user_id = 'xxx'
理论上如果写delete or update
的时候带了上面的条件,按照我们的理解那就是行锁,并发情况下只要我们参数不一致那永远不会造成死锁
但实际情况总不会如人意的,我造成死锁的2个SQL就是上面这样的操作,两个删除都是带了 where,但还是锁表了
MySQL InnoDB 引擎下,delete/update操作where后面的条件如果没有走索引,会锁表(MySQL 5.6/7 版本验证)
请注意上面我说的是 两个事物互相锁住了对方的资源
而并不是简单的两个SQL,这也就是为什么需要熟悉业务代码的人去解决,一个复杂的业务可能涉及到七八张表的操作。
上面说了死锁产生的主要原因,下面再来说说死锁在业务中产生的几种可能和解决办法
- 一般来说我们代码不管哪个线程进来都是按顺序执行的,但实际情况是我们在代码中写了各种 if else 等条件语句,这样就导致每个请求实际操作表的顺序可能不一致了,就可能造成死锁(我遇到就是这样的),但这种情况大部分我们把表索引创建好,不出现锁表就没事了。
- 并发下相同的业务参数去执行,第一个事物还没提交后面的事物又来了,这种我们加分布式锁就好了
二、死锁实践
2-1、建表
CREATE TABLE `xdx_test_one` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` varchar(32) DEFAULT NULL COMMENT '用户uuid', PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='测试表one';CREATE TABLE `xdx_test_two` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` varchar(32) DEFAULT NULL COMMENT '用户uuid', PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COMMENT='测试表two';
2-2、代码
2-2-1、controller
@GetMapping("/testLock")public void testLock(@RequestParam String params) throws InterruptedException { dataServiceImpl.testLock(params);}
2-2-2、service
@Transactional(rollbackFor = Exception.class)public void testLock(String params) throws InterruptedException { if ("one".equals(params)) { dataMapper.deleteOne(UUID.randomUUID().toString().substring(10, 18)); System.out.println(params + " 获取 xdx_test_one 表锁"); Thread.sleep(2000); } dataMapper.deleteTwo(UUID.randomUUID().toString().substring(10, 18)); System.out.println(params + " 获取 xdx_test_two 表锁"); if ("two".equals(params)) { dataMapper.deleteOne(UUID.randomUUID().toString().substring(10, 18)); System.out.println(params + " 获取 xdx_test_one 表锁"); }}
2-2-3、mapper
<delete id="deleteOne"> DELETE FROM xdx_test_one WHERE user_id = #{substring}delete><delete id="deleteTwo"> DELETE FROM xdx_test_two WHERE user_id = #{substring}delete>
2-3、测试
按照顺序快速的请求下面的url, 你就会看到上面的异常了
http://127.0.0.1:8080/testLock?parmas=one
http://127.0.0.1:8080/testLock?parmas=two
2-4、其它
我们要解决上面的死锁其实很简单,只需要给上面的 user_id 加一个索引就好了
三、死锁分析
3-1、阿里云日志分析
死锁我们一般是要看日志进行分析处理,其实只要找到两个事物之间谁拥有了对方的哪个锁即可,但原始的MySQL日志不大好看,刚好我们是使用的阿里云的服务版MySQL,它会把死锁日志进行分析以表格的方式展示,刚刚我们测试的死锁,表格呈现如下
可以先大致看一下这个表结构,应该是很清晰的,不过这个表是存在一点问题的,那就是 事物1 是持有 yx_mc.xdx_test_two 的锁
不然是不构成死锁,这个阿里云解释说我们使用的数据库版本比较老,在新的版本已经解决了这个问题。
3-2、原始日志分析
看完了上面的表,我们对死锁的组成应该有了进一步的理解,下面我们来看看MySQL的原始死锁日志
------------------------LATEST DETECTED DEADLOCK------------------------2022-06-16 17:25:01 7fde0b4a3700*** (1) TRANSACTION:TRANSACTION 36291462, ACTIVE 0.717 sec fetching rowsmysql tables in use 1, locked 1LOCK WAIT 5 lock struct(s), heap size 1184, 6 row lock(s), undo log entries 5LOCK BLOCKING MySQL thread id: 576129097 block 307687953MySQL thread id 307687953, OS thread handle 0x7fdeda9fa700, query id 1317704191 10.19.22.58 yx_property_test updatingDELETE FROM xdx_test_one WHERE user_id = '11111'*** (1) WAITING FOR THIS LOCK TO BE GRANTED:RECORD LOCKS space id 3212 page no 3 n bits 88 index `PRIMARY` of table `yx_mc`.`xdx_test_one` trx id 36291462 lock_mode X locks rec but not gap waitingRecord lock, heap no 12 PHYSICAL RECORD: n_fields 4; compact format; info bits 32 0: len 4; hex 8000000b; asc ;; 1: len 6; hex 00000229c378; asc ) x;; 2: len 7; hex 0f0000001b0c25; asc %;; 3: len 5; hex 3131313131; asc 11111;;*** (2) TRANSACTION:TRANSACTION 36291448, ACTIVE 2.005 sec fetching rowsmysql tables in use 1, locked 15 lock struct(s), heap size 1184, 7 row lock(s), undo log entries 6MySQL thread id 576129097, OS thread handle 0x7fde0b4a3700, query id 1317704227 10.19.22.58 yx_property_test updatingDELETE FROM xdx_test_two WHERE user_id = '22222'*** (2) HOLDS THE LOCK(S):RECORD LOCKS space id 3212 page no 3 n bits 88 index `PRIMARY` of table `yx_mc`.`xdx_test_one` trx id 36291448 lock_mode X locks rec but not gapRecord lock, heap no 12 PHYSICAL RECORD: n_fields 4; compact format; info bits 32 0: len 4; hex 8000000b; asc ;; 1: len 6; hex 00000229c378; asc ) x;; 2: len 7; hex 0f0000001b0c25; asc %;; 3: len 5; hex 3131313131; asc 11111;;Record lock, heap no 13 PHYSICAL RECORD: n_fields 4; compact format; info bits 32 0: len 4; hex 8000000c; asc ;; 1: len 6; hex 00000229c378; asc ) x;; 2: len 7; hex 0f0000001b0c48; asc H;; 3: len 5; hex 3131313131; asc 11111;;Record lock, heap no 14 PHYSICAL RECORD: n_fields 4; compact format; info bits 32 0: len 4; hex 8000000d; asc ;; 1: len 6; hex 00000229c378; asc ) x;; 2: len 7; hex 0f0000001b0c6b; asc k;; 3: len 5; hex 3131313131; asc 11111;;Record lock, heap no 15 PHYSICAL RECORD: n_fields 4; compact format; info bits 32 0: len 4; hex 8000000e; asc ;; 1: len 6; hex 00000229c378; asc ) x;; 2: len 7; hex 0f0000001b0c8e; asc ;; 3: len 5; hex 3131313131; asc 11111;;Record lock, heap no 16 PHYSICAL RECORD: n_fields 4; compact format; info bits 32 0: len 4; hex 8000000f; asc ;; 1: len 6; hex 00000229c378; asc ) x;; 2: len 7; hex 0f0000001b0cb1; asc ;; 3: len 5; hex 3131313131; asc 11111;;Record lock, heap no 17 PHYSICAL RECORD: n_fields 4; compact format; info bits 32 0: len 4; hex 80000010; asc ;; 1: len 6; hex 00000229c378; asc ) x;; 2: len 7; hex 0f0000001b0cd4; asc ;; 3: len 5; hex 3131313131; asc 11111;;*** (2) WAITING FOR THIS LOCK TO BE GRANTED:RECORD LOCKS space id 3213 page no 3 n bits 88 index `PRIMARY` of table `yx_mc`.`xdx_test_two` trx id 36291448 lock_mode X locks rec but not gap waitingRecord lock, heap no 18 PHYSICAL RECORD: n_fields 4; compact format; info bits 32 0: len 4; hex 80000017; asc ;; 1: len 6; hex 00000229c386; asc ) ;; 2: len 7; hex 190000002116e0; asc ! ;; 3: len 5; hex 3232323232; asc 22222;;*** WE ROLL BACK TRANSACTION (1)
N、其它
N-1、是不是真的需要事物
其实我们可以反过来想一想,我们的代码是不是一定要加锁,有时候一个复杂的业务,你加锁,会不断的出现死锁,很难根除。
换个角度,如果我们去掉事物,是不是就没有死锁了?你可能觉得不加事物数据会异常,但是想一想如果频繁产生死锁你的数据不也没办法保证吗?如果我们把所有的异常都解决是不是就没问题了?
N-2、拆分事物的颗粒度
同上一个,如果事物粒度太大了,造成死锁的可能性就很大。我们可以根据业务去进行细分,把一个大的事物拆分成多个小的事物,以此来减少死锁的可能性。
N-3、如果只有一个表是否会死锁
只操作一个表是不会死锁的,第一个事物获取了锁,第二个事物就会进入等待,所以不会出现死锁。
N-4、同一个代码是不是一定会造成死锁
我们知道MySQL里面有一个关键字 EXPLAIN
,它可以分析SQL执行的结果,但实际上一定就按照这个去执行么?其实不然的,还有一个SQL优化器,最终执行的方式是由它来决定的。
上面同样的代码,在我本地数据库 where 后面的条件不管是什么都会造成死锁,而在阿里云的数据库上不会,必须要扫描到真实存在数据才会死锁。
N-5、产生死锁的两个事物怎么办?
从目前的数据来看,事物1 会执行成功,事物2 会回滚
来源地址:https://blog.csdn.net/Tomwildboar/article/details/125299471