分析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基础(一)
Apr 05 MySQL
mysql的MVCC多版本并发控制的实现
Apr 14 MySQL
MySQL中distinct与group by之间的性能进行比较
May 26 MySQL
python中的mysql数据库LIKE操作符详解
Jul 01 MySQL
MySQL系列之六 用户与授权
Jul 02 MySQL
mysql timestamp比较查询遇到的坑及解决
Nov 27 MySQL
MySQL中int (10) 和 int (11) 的区别
Jan 22 MySQL
mysql 获取时间方式
Mar 20 MySQL
MySql中的json_extract函数处理json字段详情
Jun 05 MySQL
MySQL串行化隔离级别(间隙锁实现)
Jun 16 MySQL
MySQL事务的隔离级别详情
Jul 15 MySQL
MySQL新手入门进阶语句汇总
Sep 23 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中explode与split的区别介绍
2012/10/03 PHP
Laravel框架中Blade模板的用法示例
2017/08/30 PHP
PHP实现数组转JSon和JSon转数组的方法示例
2018/06/14 PHP
php 多继承的几种常见实现方法示例
2019/11/18 PHP
php让json_encode不自动转义斜杠“/”的方法
2020/04/27 PHP
javascript Split方法,indexOf方法、lastIndexOf 方法和substring 方法
2009/03/21 Javascript
jquery 多行滚动代码(附详细解释)
2010/06/17 Javascript
仿谷歌主页js动画效果实现代码
2013/07/14 Javascript
javascript正则匹配汉字、数字、字母、下划线
2014/04/10 Javascript
浅析javascript中function 的 length 属性
2014/05/27 Javascript
javascript设置连续两次点击按钮时间间隔的方法
2014/10/28 Javascript
jQuery获取checkbox选中的值
2016/01/28 Javascript
javascript基本语法
2016/05/31 Javascript
AngularJS 所有版本下载地址
2016/09/14 Javascript
JavaScript定时器实现的原理分析
2016/12/06 Javascript
无阻塞加载js,防止因js加载不了影响页面显示的问题
2016/12/18 Javascript
webpack配置的最佳实践分享
2017/04/21 Javascript
微信小程序自定义底部弹出框
2020/11/16 Javascript
详解项目升级到vue-cli3的正确姿势
2019/01/28 Javascript
layui加载表格,绑定新增,编辑删除,查看按钮事件的例子
2019/09/06 Javascript
TensorFlow Session使用的两种方法小结
2018/07/30 Python
Python利用递归实现文件的复制方法
2018/10/27 Python
Python发展史及网络爬虫
2019/06/19 Python
基于 HTML5 的 WebGL 3D 版俄罗斯方块的示例代码
2018/05/28 HTML / CSS
生物制药专业求职信
2014/03/11 职场文书
小学教师师德演讲稿
2014/05/06 职场文书
低碳生活的宣传标语
2014/06/23 职场文书
标准毕业生自荐信
2014/06/24 职场文书
名人演讲稿范文
2014/09/16 职场文书
2015元旦家电促销活动策划方案
2014/12/09 职场文书
2015年父亲节活动总结
2015/02/12 职场文书
小英雄雨来观后感
2015/06/09 职场文书
2016年元旦致辞
2015/08/01 职场文书
七年级作文之环保作文
2019/10/17 职场文书
Centos环境下Postgresql 安装配置及环境变量配置技巧
2021/05/18 PostgreSQL
使用CSS实现百叶窗效果示例代码
2023/05/07 HTML / CSS