转载自  MySQL死锁如何处理

前提

笔者负责的一个系统最近有新功能上线后突然在预警模块不定时报出MySQL死锁导致事务回滚。幸亏,上游系统采用了异步推送和同步查询结合的方式,感知到推送失败及时进行了补偿。于是,笔者争取了一点时间详细分析了导致死锁的多个事务的执行时序,分析并且得出解决方案。

死锁场景复现

首先,MySQL的服务端版本是5.7(小版本可以基本忽略),使用了InnoDB。有一张用户数据表的schema设计如下(无关字段已经屏蔽掉):

CREATE TABLE `t_user_data`
(id      BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,user_id BIGINT UNSIGNED NOT NULL COMMENT '用户ID',data_id VARCHAR(50)     NOT NULL COMMENT '数据ID',INDEX idx_user_id (user_id),INDEX idx_data_id (data_id)
) COMMENT '用户数据表';

业务代码中发生死锁的伪代码如下:

process_method(dataId,userDataDtoList){start transaction:userDataDao.deleteByDataId(dataId);for dto in userDataDtoList:UserData userData = convert(dto);userDataDao.insert(dto);commit;
}

这里的逻辑是,如果已经存在对应dataId的数据要先进行删除,然后写入新的用户数据。

尝试用两个Session提交两个事务重现死锁问题:

时间序列 Tx-Session-1 Tx-Session-2
T1 START TRANSACTION;  
T2   START TRANSACTION;
T3 DELETE FROM t_user_data WHERE data_id = ‘xxxxx’;  
T4   DELETE FROM t_user_data WHERE data_id = ‘yyyyy’;
T5 INSERT INTO t_user_data(USER_ID, DATA_ID) VALUES (1, ‘xxxxx’);  
T6   INSERT INTO t_user_data(USER_ID, DATA_ID) VALUES (2, ‘yyyyy’);
T7   Deadlock found when trying to get lock; try restarting transaction(Rollback)
T8 COMMIT;  

这里会出现两个现象:

  1. Tx-Session-2会话T4执行完毕之后,Tx-Session-1会话T5执行的时候,Tx-Session-1会话客户端会处于阻塞状态。

  2. Tx-Session-2会话T6执行完毕之后,MySQL提示死锁事务被回滚,此时,Tx-Session-1会话客户端会解除阻塞。

导致死锁的原因

后面会写一篇专门的文章学习和理解MySQL的InnoDB数据引擎的锁相关知识,这里直接排查InnoDB的死锁日志。

mysql> show engine innodb status;

输出的死锁日志如下:

------------------------
LATEST DETECTED DEADLOCK
------------------------
2019-05-11 19:16:04 0x5804
*** (1) TRANSACTION:
TRANSACTION 3882, ACTIVE 13 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 32, OS thread handle 9876, query id 358 localhost ::1 doge update
INSERT INTO t_user_data(USER_ID, DATA_ID) VALUES (1, 'xxxxx')
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 33 page no 6 n bits 72 index idx_data_id of table `test`.`t_user_data` trx id 3882 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 00: len 8; hex 73757072656d756d; asc supremum;;*** (2) TRANSACTION:
TRANSACTION 3883, ACTIVE 9 sec inserting, thread declared inside InnoDB 5000
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 11, OS thread handle 22532, query id 359 localhost ::1 doge update
INSERT INTO t_user_data(USER_ID, DATA_ID) VALUES (2, 'yyyyy')
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 33 page no 6 n bits 72 index idx_data_id of table `test`.`t_user_data` trx id 3883 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 00: len 8; hex 73757072656d756d; asc supremum;;*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 33 page no 6 n bits 72 index idx_data_id of table `test`.`t_user_data` trx id 3883 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 00: len 8; hex 73757072656d756d; asc supremum;;*** WE ROLL BACK TRANSACTION (2)

这里要参考MySQL关于InnoDB锁的关于next-key锁描述那一节,注意死锁日志关键字supremum的意义:

next-key锁将gap锁定在索引中最大值之上,而supremum伪记录的值高于索引中实际的任何值。supremum不是真正的索引记录,因此,实际上,此next-key锁仅锁定最大索引值之后的间隙。

两个事务的锁属性可以通过select * from information_schema.innodb_locks;进行查询,数据如下表:

lock_id lock_tx_id lock_mode lock_type lock_table lock_index lock_space lock_page lock_rec lock_data
3882:33:6:1 3882 X RECORD test.t_user_data idx_data_id 33 6 1 supremum pseudo-record
3883:33:6:1 3883 X RECORD test.t_user_data idx_data_id 33 6 1 supremum pseudo-record
DELETE FROM t_user_data WHERE data_id = '不存在的索引值';

上面的SQL执行时候,如果条件刚好是索引列,并且查询的值是当前表(索引)中不存在的数据,根据next-key锁的描述和死锁日志中的asc supremum关键字,执行该DELETE语句的时候,会锁定目标值和高于目标值的任何值,如果条件是"xxxxx",那么相当于锁定区间为(“xxxxx”,最大上界]。

next-key锁是索引记录上的记录锁(Record Lock)和索引记录之前的间隙上的间隙锁(Gap Lock)定的组合。间隙锁有两个特点:

  1. 两个事务即使锁定的区间一致(或者有部分重合),不会影响它们之间获取到锁(可以参考行锁的兼容性矩阵)。

  2. 间隙锁G会阻止非持有G的其他事务向锁定的区间中插入数据,以避免产生冲突数据。

分析到这里,就很好解释上面出现死锁的执行时序:

  1. 两个事务的DELETE语句都可以正确执行,这个时候,两者的间隙锁锁定的区域分别是(‘xxxxx’,最大上界]和(‘yyyyy’,最大上界]。

  2. 事务1执行INSERT语句的时候阻塞,是因为事务2的间隙锁不允许事务1插入索引值’xxxxx’。

  3. 事务2执行INSERT语句的时候阻塞,是因为事务1的间隙锁不允许事务1插入索引值’yyyyy’,执行到这一步,MySQL的死锁检查模块应该起效了,因为两个事务依赖的锁资源已经成环(或者成有向图)。

  4. 事务2的优先级比较低,于是抛出死锁异常并且被回滚了。

之前曾经和DBA同事聊过,发生死锁的事务是怎么衡量优先级或者怎么确定哪个事务需要回滚(释放锁资源让另一个事务可以正常提交),但是后来没有收到很好的答复,这一点有时间再研究一下。

解决方案

参考MySQL的文档,解决方案有两个:

  1. 方案一:降低数据库的事务隔离级别,需要降低到READ COMMITED,这样子可以关闭间隙锁的扫描。(<== 并不推荐这种做法,修改事务隔离级别有可能出现新的问题)

  2. 方案二:针对对应的原因修改业务代码。

这里方案二只需要把伪代码逻辑修改如下:

process_method(dataId,userDataDtoList){List<UserData> userDataList = userDataDao.selectByDataId(dataId);start transaction:if userDataList is not empty: List<Long> ids = collectIdList(userDataList);userDataDao.deleteByIds(ids);       for dto in userDataDtoList:UserData userData = convert(dto);userDataDao.insert(dto);commit;
}

就是先根据dataId进行查询,如果存在数据,聚合主键列表,通过主键列表进行删除,然后再进行数据插入。

小结

这并非是第一次在生产环境中出现MySQL死锁,只是这次的案例相对简单。InnoDB提供的死锁日志其实并没有提供完整的事务提交的SQL,所以对于复杂的场景需要细致结合代码和死锁日志进行排查,很多时候对应的代码逻辑是多处的。这里列举一下笔者处理死锁问题的一些步骤:

  1. 及时止损,如果可以回滚导致死锁的代码,那么最好果敢地回滚;如果重试可以解决问题并且出现死锁问题的规模不大,可以尝试短时间内进行问题排查。

  2. 通过业务系统日志迅速定位到发生死锁的代码块,JVM应用一般底层是依赖JDBC,出现死锁的时候会抛出一个SQLException的子类,异常栈的信息中带有"Deadlock"字样。

  3. 分析InnoDB的死锁日志,一般会列出竞争锁的多个事务的相对详细的信息,这些信息是排查死锁问题的第一手资料。

  4. 修复问题上线后注意做好监控和预警,确定问题彻底解决。

参考资料:

  • MySQL5.7官方文档

MySQL死锁如何处理相关推荐

  1. mysql 死锁是什么_mysql死锁是什么意思

    mysql死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环.InnoDB存储引擎能检测到死锁的循环依赖并立即返回一个错误.只有部分或完全回滚其中一个事务,才能打破 ...

  2. mysql死锁解决方法_MySQL死锁及解决方案

    一.MySQL锁类型 1. MySQL常用存储引擎的锁机制 MyISAM和MEMORY采用表级锁(table-level locking) BDB采用页面锁(page-level locking)或表 ...

  3. java mysql死锁_记一次线上mysql死锁分析(一)

    记录一次比较诡异的mysql死锁日志.系统运行几个月来,就在前几天发生了一次死锁,而且就只发生了一次死锁,整个排查过程耗时将近一天,最后感谢我们的DBA大神和老大一起分析找到原因. 诊断死锁 借助于我 ...

  4. mysql查询死锁的次数_一次神奇的MySQL死锁排查记录

    一次神奇的MySQL死锁排查记录 发布时间:2020-08-29 00:50:26 来源:脚本之家 阅读:135 作者:咖啡拿铁 背景 说起Mysql死锁,之前写过一次有关Mysql加锁的基本介绍,对 ...

  5. mysql 死锁监视器_并发基础知识:死锁和对象监视器

    mysql 死锁监视器 本文是我们名为Java Concurrency Essentials的学院课程的一部分. 在本课程中,您将深入探讨并发的魔力. 将向您介绍并发和并发代码的基础知识,并学习诸如原 ...

  6. mysql死锁的排查方法_MySQL死锁系列-线上死锁问题排查思路

    前言 MySQL 死锁异常是我们经常会遇到的线上异常类别,一旦线上业务日间复杂,各种业务操作之间往往会产生锁冲突,有些会导致死锁异常.这种死锁异常一般要在特定时间特定数据和特定业务操作才会复现,并且分 ...

  7. go语言 mysql卡死_一次mysql死锁的排查过程-Go语言中文社区

    一次mysql死锁的排查过程一.背景17号晚上要吃饭了,看旁边的妹子和佐哥还在调代码,就问了下什么问题啊,还在弄,妹子说,在测试环境测试给用户并发发送卡券时,出现了死锁,但看代码没有死锁,问题如下图 ...

  8. zabbix监控mysql死锁

    percona MySQL Server Template算是比较常用的zabbix监控mysql的模板了,监控项也比较齐全,但是没有监控mysql死锁的监控项,如果有需求,就需要另外创建模板或者监控 ...

  9. 一次MySQL死锁问题解决

    一次MySQL死锁问题解决 一.环境 CentOS, MySQL 5.6.21-70, JPA 问题场景:系统有定时批量更新数据状态操作,每次更新上千条记录,表中总记录数约为500W左右. 二.错误日 ...

最新文章

  1. .data和.text段合并
  2. Python初学者必学的20个重要技巧
  3. Java开发面经分享:SpringIOC中复杂属性如何“巧妙
  4. 安装python应该先安装pycharm还是python_Pycharm及python安装详细步骤及PyCharm配置整理(推荐)...
  5. ES6笔记 -- 字符串拓展
  6. mysql 实现master-slave 同步
  7. 腾讯专家详解Angel平台实操技巧,助你比赛一马当先!
  8. storm发布jar包时报找不到主类_咖啡5元一大包,进口饼干10元3包…济南有个临期食品超市,快过期的食品你会买单吗...
  9. 无需依赖Adobe Acrobat,在Java中进行PDF格式转换全新攻略
  10. 7 个优秀的 WordPress 倒计时插件
  11. 关于几种图片格式的压缩
  12. AI遇上农业会怎样?最新UNT《智慧农业》2022全面综述农业4.0发展的架构、技术、应用等
  13. [转]微服务概念解析
  14. 已有一个排好序的数组,由键盘输入一个数,要求按原来的排序规律将其插入到数组中.
  15. 缓存的穿透、击穿、雪崩分别是什么,有什么解决方法
  16. 我的职业是计算机英语,职业英语系列:计算机英语
  17. 论文当中图片保存png、pdf等等的要分辨率DPI
  18. 极狐gitlab版本升级 #JIHULAB101
  19. 什么是瞬时极性法,怎么使用?
  20. 一个超简单的反编译任务(IDAPro、X32dbg)

热门文章

  1. 听红宝书译者谈Web视角下的前端开发
  2. [Java基础]接口组成(默认方法,静态方法,私有方法)
  3. 《C++ Primer》13.1.2节练习
  4. AcWing 1015. 摘花生
  5. [蓝桥杯2019初赛]最大降雨量-模拟
  6. 数位dp总结 之 从入门到模板(stO)
  7. MATLAB-矩阵基本语法知识
  8. Matlab出现On Startup: Error using eval undefined function 'workspacefunc' for input arguments of type
  9. OpenCV Stitching 工程搭建
  10. P4719 【模板】“动态 DP“动态树分治(矩阵/轻重链剖分/ddp)