分析MySQL优化 index merge 后引起的死锁


Posted in MySQL onApril 19, 2022

背景

生产环境出现死锁流水,通过查看死锁日志,看到造成死锁的是两条一样的update语句(只有where条件中的值不同),

如下:

UPDATE test_table SET `status` = 1 WHERE `trans_id` = 'xxx1' AND `status` = 0;
UPDATE test_table SET `status` = 1 WHERE `trans_id` = 'xxx2' AND `status` = 0;

一开始比较费解,通过大量查询跟学习后,分析出了死锁形成的具体原理,特分享给大家,希望能帮助到遇到同样问题的朋友。

因为MySQL知识点较多,这里对很多名词不进行过多介绍,有兴趣的朋友,可以后续进行专项深入学习。

死锁日志

*** (1) TRANSACTION:
TRANSACTION 791913819, ACTIVE 0 sec starting index read, thread declared inside InnoDB 4999
mysql tables in use 3, locked 3
LOCK WAIT 4 lock struct(s), heap size 1184, 3 row lock(s)
MySQL thread id 462005230, OS thread handle 0x7f55d5da3700, query id 2621313306 x.x.x.x test_user Searching rows for update
UPDATE test_table SET `status` = 1 WHERE `trans_id` = 'xxx1' AND `status` = 0;
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 110 page no 39167 n bits 1056 index `idx_status` of table `test`.`test_table` trx id 791913819 lock_mode X waiting
Record lock, heap no 495 PHYSICAL RECORD: n_fields 2; compact format; info bits 0

*** (2) TRANSACTION:
TRANSACTION 791913818, ACTIVE 0 sec starting index read, thread declared inside InnoDB 4999
mysql tables in use 3, locked 3
5 lock struct(s), heap size 1184, 4 row lock(s)
MySQL thread id 462005231, OS thread handle 0x7f55cee63700, query id 2621313305 x.x.x.x test_user Searching rows for update
UPDATE test_table SET `status` = 1 WHERE `trans_id` = 'xxx2' AND `status` = 0;
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 110 page no 39167 n bits 1056 index `idx_status` of table `test`.`test_table` trx id 791913818 lock_mode X
Record lock, heap no 495 PHYSICAL RECORD: n_fields 2; compact format; info bits 0

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 110 page no 41569 n bits 88 index `PRIMARY` of table `test`.`test_table` trx id 791913818 lock_mode X locks rec but not gap waiting
Record lock, heap no 14 PHYSICAL RECORD: n_fields 30; compact format; info bits 0

*** WE ROLL BACK TRANSACTION (1)

简要分析下上边的死锁日志:

  • 1、第一块内容(第1行到第9行)中,第6行为事务(1)执行的SQL语句,第7和第8行意思为事务(1)在等待 idx_status 索引上的X锁;
  • 2、第二块内容(第11行到第19行)中,第16行为事务(2)执行的SQL语句,第17和第18行意思为事务(2)持有 idx_status 索引上的X锁;
  • 3、第三块内容(第21行到第23行)的意思为,事务(2)在等待 PRIMARY 索引上的X锁。(but not gap指不是间隙锁)
  • 4、最后一句的意思即为,MySQL将事务(1)进行了回滚操作。

表结构

CREATE TABLE `test_table` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`trans_id` varchar(21) NOT NULL,
`status` int(11) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_trans_id` (`trans_id`) USING BTREE,
KEY `idx_status` (`status`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8

通过表结构可以看出,trans_id 列上有一个唯一索引uniq_trans_id status 列上有一个普通索引idx_status ,id列为主键索引 PRIMARY

InnoDB引擎中有两种索引:

  • 聚簇索引: 将数据存储与索引放到了一块,索引结构的叶子节点保存了行数据。
  • 辅助索引: 辅助索引叶子节点存储的是主键值,也就是聚簇索引的键值。

主键索引 PRIMARY 就是聚簇索引,叶子节点中会保存行数据。uniq_trans_id 索引和idx_status 索引为辅助索引,叶子节点中保存的是主键值,也就是id列值。

当我们通过辅助索引查找行数据时,先通过辅助索引找到主键id,再通过主键索引进行二次查找(也叫回表),最终找到行数据。

执行计划

分析MySQL优化 index merge 后引起的死锁

通过看执行计划,可以发现,update语句用到了索引合并,也就是这条语句既用到了 ​​uniq_trans_id​​​ 索引,又用到了 ​​idx_status​​​ 索引,​​Using intersect(uniq_trans_id,idx_status)​​的意思是通过两个索引获取交集。

为什么会用 index_merge(索引合并)

MySQL5.0之前,一个表一次只能使用一个索引,无法同时使用多个索引分别进行条件扫描。但是从5.1开始,引入了 ​​index merge​​ 优化技术,对同一个表可以使用多个索引分别进行条件扫描。

如执行计划中的语句:

UPDATE test_table SET `status` = 1 WHERE `trans_id` = '38' AND `status` = 0 ;

MySQL会根据 ​​trans_id = ‘38’​​​这个条件,利用 ​​uniq_trans_id​​​ 索引找到叶子节点中保存的id值;同时会根据 ​​status = 0​​​这个条件,利用 ​​idx_status​​ 索引找到叶子节点中保存的id值;然后将找到的两组id值取交集,最终通过交集后的id回表,也就是通过 PRIMARY 索引找到叶子节点中保存的行数据。

这里可能很多人会有疑问了,​​uniq_trans_id​​​ 已经是一个唯一索引了,通过这个索引最终只能找到最多一条数据,那MySQL优化器为啥还要用两个索引取交集,再回表进行查询呢,这样不是多了一次 ​​idx_status​​ 索引查找的过程么。我们来分析一下这两种情况执行过程。

第一种 只用uniq_trans_id索引 :

  • 根据 ​​trans_id = ‘38’​​查询条件,利用​​uniq_trans_id​​ 索引找到叶子节点中保存的id值;
  • 通过找到的id值,利用PRIMARY索引找到叶子节点中保存的行数据;
  • 再通过 ​​status = 0​​ 条件对找到的行数据进行过滤。

第二种 用到索引合并 ​​Using intersect(uniq_trans_id,idx_status)​​:

  • 根据 ​​trans_id = ‘38’​​ 查询条件,利用 ​​uniq_trans_id​​ 索引找到叶子节点中保存的id值;
  • 根据 ​​status = 0​​ 查询条件,利用 ​​idx_status​​ 索引找到叶子节点中保存的id值;
  • 将1/2中找到的id值取交集,然后利用PRIMARY索引找到叶子节点中保存的行数据

上边两种情况,主要区别在于,第一种是先通过一个索引把数据找到后,再用其它查询条件进行过滤;第二种是先通过两个索引查出的id值取交集,如果取交集后还存在id值,则再去回表将数据取出来。

当优化器认为第二种情况执行成本比第一种要小时,就会出现索引合并。(生产环境流水表中 ​​status = 0​​ 的数据非常少,这也是优化器考虑用第二种情况的原因之一)。

为什么用了 ​​index_merge​​ 就死锁了

分析MySQL优化 index merge 后引起的死锁

上面简要画了一下两个update事务加锁的过程,从图中可以看到,在​​idx_status​​ 索引和 PRIMARY (聚簇索引) 上都存在重合交叉的部分,这样就为死锁造成了条件。

如,当遇到以下时序时,就会出现死锁:

分析MySQL优化 index merge 后引起的死锁

事务1等待事务2释放锁,事务2等待事务1释放锁,这样就造成了死锁。

MySQL检测到死锁后,会自动回滚代价更低的那个事务,如上边的时序图中,事务1持有的锁比事务2少,则MySQL就将事务1进行了回滚。

解决方案

一、从代码层面

  • where 查询条件中,只传 ​​trans_id​​ ,将数据查询出来后,在代码层面判断 status 状态是否为0;
  • 使用 ​​force index(uniq_trans_id)​​ 强制查询语句使用 ​​uniq_trans_id​​ 索引;
  • where 查询条件后边直接用 id 字段,通过主键去更新。

二、从MySQL层面

  • 删除 ​​idx_status​​ 索引或者建一个包含这俩列的联合索引;
  • 将MySQL优化器的​​index merge​​优化关闭。

到此这篇关于MySQL 优化 index merge引起的死锁分析的文章就介绍到这了!

MySQL 相关文章推荐
MySQL 覆盖索引的优点
May 19 MySQL
MySQL 5.7常见数据类型
Jul 15 MySQL
sql注入教程之类型以及提交注入
Aug 02 MySQL
一篇文章带你深入了解Mysql触发器
Aug 02 MySQL
mysql事务隔离级别详情
Oct 24 MySQL
MySQL表类型 存储引擎 的选择
Nov 11 MySQL
MySQL 主从复制数据不一致的解决方法
Mar 18 MySQL
MySQL学习之基础命令实操总结
Mar 19 MySQL
讲解MySQL增删改操作
May 06 MySQL
MySQL数据库中的锁、解锁以及删除事务
May 06 MySQL
MySQL运行报错:“Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggre”解决方法
Jun 14 MySQL
MySQL transaction事务安全示例讲解
Jun 21 MySQL
解决MySQL报“too many connections“错误
Mysql查询时间区间日期列表,不会由于数据表数据影响
Apr 19 #MySQL
WINDOWS下安装mysql 8.x 的方法图文教程
CentOS MySql8 远程连接实战
Mysql排查分析慢sql之explain实战案例
Apr 19 #MySQL
MySQL视图概念以及相关应用
mysql 乱码 字符集latin1转UTF8
Apr 19 #MySQL
You might like
通过具体程序来理解PHP里面的抽象类
2010/01/28 PHP
PHP中$_SERVER的详细参数与说明介绍
2013/10/26 PHP
php实现字符串首字母大写和单词首字母大写的方法
2015/03/14 PHP
jQuery html() in Firefox (uses .innerHTML) ignores DOM changes
2010/03/05 Javascript
仅IE6/7/8中innerHTML返回值忽略英文空格的问题
2011/04/07 Javascript
分享一个用Mootools写的鼠标滑过进度条改变进度值的实现代码
2011/12/12 Javascript
Jquery中删除元素的实现代码
2011/12/29 Javascript
JQuery魔力之$("tagName")与selector
2012/03/05 Javascript
关于IE BUG与字符串截取substr的解决办法
2013/04/10 Javascript
jquery和javascript的区别(常用方法比较)
2013/07/04 Javascript
基于JQuery的列表拖动排序实现代码
2013/10/01 Javascript
JS替换字符串中空格方法
2015/04/17 Javascript
基于JavaScript实现窗口拖动效果
2017/01/18 Javascript
Bootstrap面板(Panels)的简单实现代码
2017/03/17 Javascript
Javascript别踩白块儿(钢琴块儿)小游戏实现代码
2017/07/20 Javascript
electron demo项目npm install安装失败的解决方法
2018/02/06 Javascript
node实现的爬虫功能示例
2018/05/04 Javascript
js的对象与函数详解
2019/01/21 Javascript
你可能不知道的CORS跨域资源共享
2019/03/13 Javascript
Vue实现跑马灯效果
2020/05/25 Javascript
全面了解Python的getattr(),setattr(),delattr(),hasattr()
2016/06/14 Python
详解python中的json和字典dict
2018/06/22 Python
Python函数参数匹配模型通用规则keyword-only参数详解
2019/06/10 Python
Pytorch在dataloader类中设置shuffle的随机数种子方式
2020/01/14 Python
使用python实现名片管理系统
2020/06/18 Python
Python使用socket模块实现简单tcp通信
2020/08/18 Python
详解快速开发基于 HTML5 网络拓扑图应用
2018/01/08 HTML / CSS
什么是Deployment descriptors;都有什么类型的部署描述符
2015/07/28 面试题
开会迟到检讨书
2014/01/08 职场文书
新年爱情寄语
2014/04/08 职场文书
保护环境倡议书范文
2014/05/13 职场文书
质量安全标语
2014/06/07 职场文书
抗洪救灾感谢信
2015/01/22 职场文书
职工食堂管理制度
2015/08/06 职场文书
三八红旗手主要事迹材料
2015/11/04 职场文书
Java基础——Map集合
2022/04/01 Java/Android