记录一次 mysql 死锁 (患者 为 index merge)

几点

  • 读懂死锁日志
  • 日志分析
  • 基础知识补习
  • 死锁 原因分析
  • 解决办法
  • 写在最后的 锁

读懂死锁日志

第一步 先登录上 公司 的yearing 审核平台

  • 通过 show engine innodb status;

经过简单的格式化我们拿到一下日志

 2019-09-22 04:00:05 0x2b9980b91700 
 *** (1) TRANSACTION: TRANSACTION 131690250, ACTIVE 0 sec fetching rows mysql tables in use 3, locked 3 

 LOCK WAIT 73 lock struct(s), heap size 8400, 
 3 row lock(s) 
 MySQL thread id 518726, OS thread handle 47938289870592, query id 813732402 172.31.16.205 ops_write updating 

update msgbox_message set record_status = -1 where record_status = 0 and gmt_create >= now() - INTERVAL 3 MONTH and gmt_create < now() + INTERVAL 1 HOUR and msg_key = 'orderMsg:return:SON19091204222951376' and target_type = 0 and target = '171064635';

*** (1) WAITING FOR THIS LOCK TO BE GRANTED: 
RECORD LOCKS space id 118 page no 451030 n bits 640 index target of table `cf_msgbox`.`msgbox_message` 
/* Partition `p1910` */ 
trx id 131690250 lock_mode X locks rec but not gap waiting Record lock, 

heap no 569 PHYSICAL RECORD: n_fields 4; compact format;
 info bits 0 
0: len 9; hex 313731303634363335; asc 171064635;; 
1: len 1; hex 00; asc ;; 
2: len 8; hex 00000000031952bc; asc R ;; 
3: len 4; hex 5d79c74f; asc ]y O;; 


*** (2) TRANSACTION: 
TRANSACTION 131690230, ACTIVE 1 sec fetching rows mysql tables in use 3, 
locked 3 70 lock struct(s), heap size 8400, 
7 row lock(s) 
MySQL thread id 501733, 
OS thread handle 47938289604352, 
query id 813732142 172.31.29.84 ops_write updating 

update msgbox_message set record_status = -1 where record_status = 0 and gmt_create >= now() - INTERVAL 3 MONTH and gmt_create < now() + INTERVAL 1 HOUR and msg_key = 'orderMsg:return:SON19091204192462327' and target_type = 0 and target = '171064635' 


*** (2) HOLDS THE LOCK(S): 
RECORD LOCKS space id 118 page no 451030 n bits 640 index target of table `cf_msgbox`.`msgbox_message` 
/* Partition `p1910` */ 
trx id 131690230 lock_mode X locks rec but not gap Record lock, 
heap no 569 PHYSICAL RECORD: n_fields 4; compact format; info bits 0 

0: len 9; hex 313731303634363335; asc 171064635;; 
1: len 1; hex 00; asc ;; 
2: len 8; hex 00000000031952bc; asc R ;; 
3: len 4; hex 5d79c74f; asc ]y O;; 

Record lock, heap no 571 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
0: len 9; hex 313731303634363335; asc 171064635;; 
1: len 1; hex 00; asc ;; 
2: len 8; hex 00000000031962fc; asc b ;; 
3: len 4; hex 5d79c808; asc ]y ;; 

*** (2) 
WAITING FOR THIS LOCK TO BE GRANTED: 
RECORD LOCKS space id 118 page no 1283285 n bits 152 index PRIMARY of table `cf_msgbox`.`msgbox_message` 
/* Partition `p1910`*/ 
trx id 131690230 lock_mode X locks rec but not gap waiting Record lock, 
heap no 60 PHYSICAL RECORD: n_fields 18; compact format; info bits 0 
0: len 8; hex 00000000031962fc; asc b ;; 
1: len 4; hex 5d79c808; asc ]y ;; 
2: len 6; hex 000006232f37; asc #/7;; 
3: len 7; hex 72000003330b29; asc r 3 );; 
4: len 4; hex 7fffffff; asc ;; 
5: len 4; hex 5d7c3e27; asc ]|>';; 
6: len 30; hex 6f726465724d73673a72657475726e3a534f4e3139303931323034323232; asc orderMsg:return:SON19091204222; (total 36 bytes); 
7: len 5; hex 6f72646572; asc order;; 
8: len 1; hex 00; asc ;; 
9: len 9; hex 313731303634363335; asc 171064635;; 
10: len 15; hex 52657475726e204175646974696e67; asc Return Auditing;; 
11: len 15; hex 52657475726e204175646974696e67; asc Return Auditing;; 
12: len 30; hex 2f6f7264657273232f534f3133343830393937322f72657475726e5f7265; asc /orders#/SO134809972/return_re; (total 34 bytes); 
13: len 4; hex 5dca3388; asc ] 3 ;; 
14: SQL NULL; 
15: len 13; hex 6f726465725f6d657373616765; asc order_message;; 
16: len 8; hex 8000000003195e72; asc ^r;; 
17: len 18; hex 63662d736572762d7573657263656e746572; asc cf-serv-usercenter;; 
*** WE ROLL BACK TRANSACTION (2) 

背景:接收订单变化 更新订单消息(事物所涉及代码就一行update语句)

大致表结构:

CREATE TABLE `msgbox_message` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `record_status` int(11) NOT NULL DEFAULT '0' COMMENT '0 正常   -1删除',
  `gmt_create` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `gmt_modify` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `msg_key` varchar(64)  DEFAULT NULL 
  `box` varchar(64)  NOT NULL COMMENT 'box key',
  `target_type` tinyint(3) unsigned NOT NULL ,
  `target` varchar(32)  NOT NULL ,
  `disappear_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
  PRIMARY KEY (`id`,`gmt_modify`),
  KEY `target` (`target`,`target_type`),
  KEY `msg_key` (`msg_key`),
  KEY `box_key` (`box`),
  KEY `gmt_modify` (`gmt_modify`),
  KEY `disappear_at` (`disappear_at`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='消息记录表'

大致一看,查询使用同一索引,最多是一个Block,报TimeOut的错误才对,怎么报DeadLock?

日志分析 注释:

- (1) TRANSACTION:此处表示事务1开始 ;
- MySQL thread id 518726, OS thread handle 47938289870592, query id 813732402 172.31.16.205 ops_write updating 此处为记录当前数据库线程id;
- update msgbox_message set record_status = -1 where record_status = 0 and gmt_create >= now() - INTERVAL 3 MONTH and gmt_create < now() + INTERVAL 1 HOUR and msg_key = 'orderMsg:return:SON19091204192462327' and target_type = 0 and target = '171064635'  
: 表示事务1在执行的sql,通过show engine innodb status 是查看不到完整的事务的sql的,
  通常显示当前正在等待锁的sql;不过在本案中  我们只涉及到这一个sql;
- (1) WAITING FOR THIS LOCK TO BE GRANTED: 此处表示当前事务1等待获取行锁;
本案中 等待 index target 上 lock_mode X locks rec but not gap(Record Locks)

- (2) TRANSACTION:此处表示事务2开始 ;
- update msgbox_message set record_status = -1 where record_status = 0 and gmt_create >= now() - INTERVAL 3 MONTH and gmt_create < now() + INTERVAL 1 HOUR and msg_key = 'orderMsg:return:SON19091204192462327' and target_type = 0 and target = '171064635' 
: 示事务2在执行的sql;
- (2) HOLDS THE LOCK(S):此处表示当前事务2持有的行锁;
- (2) WAITING FOR THIS LOCK TO BE GRANTED:此处表示当前事务2等待获取行锁;
本案中 等待 index PRIMARY 的lock_mode X locks rec but not gap waiting Record lock

可以看出,两个事务互相拥有对方需要的主键记录锁,
而又在等待对方的另一把锁释放,所以造成了死锁。
看一看两条语句相关的内容:

  • update msgbox_message set record_status = -1 where record_status = 0 and gmt_create >= now() - INTERVAL 3 MONTH and gmt_create < now() + INTERVAL 1 HOUR and msg_key = 'orderMsg:return:SON19091204222951376' and target_type = 0 and target = '171064635';

  • update msgbox_message set record_status = -1 where record_status = 0 and gmt_create >= now() - INTERVAL 3 MONTH and gmt_create < now() + INTERVAL 1 HOUR and msg_key = 'orderMsg:return:SON19091204192462327' and target_type = 0 and target = '171064635'
    通过 where 条件 我们可以看到,
    两条语句并没有重合的内容,感觉上不符合"常理",那么问题问题来了:

为什么还会产生死锁?

基础知识补习:

  • mysql 行锁实现机制:
    • 我们都知道的是 mysql innoDB 的行锁是通过索引上的索引项加锁来实现的。
    • innoDB 索引分为主键索引和非主键索引两种,如果一条sql语句操作了主键索引,MySQL就会锁定这条主键索引;如果一条语句操作了非主键索引,MySQL会先锁定该非主键索引,再锁定相关的主键索引。
    在innoDB中主键索引是聚簇索引,索引的叶子节点存的是整行数据。
    非主键索引的叶子节点的内容是主键的值,在InnoDB中,非主键索引也被称为非聚簇索引
    
    • innodb支持数据库ACID的特性,在数据更新时会保证数据一致性,上文中死锁的update语句不管怎么执行,数据库都会保证数据的合法性,不会使数据丢失或错误,那么它怎么来实现这点呢?
      • 简单来说,就是对数据加锁,最小粒度就是行级锁。而锁会明显削弱并发性能,为了提高并发性,MySQL实现了MVCC,也就是多版本并发控制,实现读不加锁,读写不冲突,它将读取操作分为两种:快照读,不加锁& 当前读,加锁
      • 快照读就是常规的select语句,读取时不加锁,但有可能读不到最新版本,所以叫快照读。
      • 当前读是读取数据的最新版本,有以下几种情况:
      select * from table where ? lock in share mode;
      select * from table where ? for update;
      insert into table values(…);insert可能会触发unique检查,也算当前读
      update table set ? where ?;
      delete from table where ?;
      
    • 增删改本质上有两部分: 先将数据读取出来,再对数据进行修改。前一部分读取操作是当前读,需要获取最新版本的数据。为了防止其他并发的事务对数据进行修改,当前读需要对当前数据加上互斥锁,修改完成后才将锁释放,将数据的修改串行化,保证安全。

原因分析:

出问题 的 这句sql 简化为:
update msgbox_message set record_status = -1 where
record_status = 0
and msg_key = X;
and target_type = Y and target = Z;
update 语句 首先使用了 当前读(就是读取实际的持久化的数据) 查询出要更新的记录
在这个过程 比如先根据索引 target 锁定
然后再找到索引上的 primary key 再次进行锁定。
最后 通过 primary key 更新掉我们的数据。

本次案件中
事物一等待 index target 事物二 等待 primary key
锁产生在当前读,所以需要回到执行计划,查看当前读如何进行。
(由于 Yearning 不支持 像 带 updete 的explan )
所以我们只能通过select 语句 查询到底对于update 的读取是个怎么样的过程

partitions type possible_keys key key_len ref row filtered Extra
p1910 index_merge target_key,msg_key,gmt_create target_key,msg_key 131,259 1 5 Useing intersect(target_key,msg_key),Useing where

从 执行计划中可以看到,MySQL使用了index merge
使用两个索引分别读数据,然后将数据进行intersect(取交集)。也就是说当前读发生在了两个索引上,这就是问题的关键!!!
可以看到 执行计划中 将我们target、msg_key进行了merge

因为我们使用的是两个索引,where条件中是复合条件,那么mysql会使用index merge进行优化,优化过程是mysql会先用索引1进行扫表,在用索引2进行扫表,然后求交集形成一个合并索引。这个使用索引扫表的过程和我们本身的sql使用索引的顺序可能存在互斥,所以造成了死锁。

关于索引合并

intersection 只是 索引合并中的一种,还有 union, sort_union 。
可以用到 index_merge 是有比较苛刻的条件。

  1. 首先是 Range 优先(>5.6.7)。比如 key1=1 or (key1=2 and key2=3),其中key1是可以转化成 range scan 的,不会使用 index merge union
  2. 其次,Intersect和Union要符合 ROR,即 Rowid-Ordered-Retrival:

解决方法

  1. 添加 target + msg_key的组合索引,这样就可以避免掉index merge;
  2. 或将 优化器的index merge优化关闭
  3. 先select id 然后根据id updete ;

最后的最后 记录一下知识点:

锁的种类&概念

  • Shared and Exclusive Locks

    • Shared lock: 共享锁,官方描述:permits the transaction that holds the lock to read a row

eg:select * from xx where a=1 lock in share mode

  • Exclusive Locks:排他锁:
    • permits the transaction that holds the lock to update or delete a row

eg: select * from xx where a=1 for update

  • Intention Locks

    • 这个锁是加在table上的,表示要对下一个层级(记录)进行加锁
    • Intention shared (IS):Transaction T intends to set S locks on individual rows in table t
    • Intention exclusive (IX): Transaction T intends to set X locks on those rows
    • 在数据库层看到的结果是这样的:
    TABLE LOCK table `lc_3`.`a` trx id 133588125 lock mode IX
    
  • Record Locks

    • 在数据库层看到的结果是这样的:
    RECORD LOCKS space id 281 page no 3 n bits 72 index PRIMARY of table 
    `lc_3`.`a` trx id 133588125   
    lock_mode X locks rec but not gap
    
    • 该锁是加在索引上的(从上面的index PRIMARY of table lc_3.a 就能看出来)
    • 记录锁可以有两种类型:
      • lock_mode X locks rec but not gap
      • lock_mode S locks rec but not gap
  • Gap Locks

    • 在数据库层看到的结果是这样的:
    - RECORD LOCKS space id 281 page no 5 n bits 72 index idx_c of table 
    `lc_3`.`a` trx id 133588125   
    lock_mode X locks gap before rec
    
    • Gap锁是用来防止insert的
    • Gap锁,中文名间隙锁,锁住的不是记录,而是范围,
      比如:(negative infinity, 10),(10, 11)区间,这里都是开区间哦
  • Next-Key Locks

    • 在数据库层看到的结果是这样的:
    RECORD LOCKS space id 281 page no 5 n bits 72 index idx_c of table 
    `lc_3`.`a` trx id 133588125   
    lock_mode X
    
    • Next-Key Locks = Gap Locks + Record Locks 的结合,
    • 不仅仅锁住记录,还会锁住间隙,
      比如: (negative infinity, 10】,(10, 11】区间,这些右边都是闭区间哦
  • Insert Intention Locks

    • 在数据库层看到的结果是这样的:
    RECORD LOCKS space id 279 page no 3 n bits 72 index PRIMARY of table lc_3.t1 trx id 133587907   
    lock_mode X insert intention waiting
    
    • Insert Intention Locks 可以理解为特殊的Gap锁的一种,用以提升并发写入的性能
  • AUTO-INC Locks

    • 在数据库层看到的结果是这样的:
    TABLE LOCK table xx trx id 7498948 lock mode AUTO-INC waiting
    
    • 属于表级别的锁
    • 自增锁的详细情况可以之前的一篇文章:
      http://keithlan.github.io/2017/03/03/auto_increment_lock/

记录锁,间隙锁,Next-key 锁和插入意向锁。这四种锁对应的死锁如下:

  • 记录锁(LOCK_REC_NOT_GAP): lock_mode X locks rec but not gap
  • 间隙锁(LOCK_GAP): lock_mode X locks gap before rec
  • Next-key 锁(LOCK_ORNIDARY): lock_mode X
  • 插入意向锁(LOCK_INSERT_INTENTION): lock_mode X locks gap before rec insert intention

参考资料:

  • https://www.hollischuang.com/archives/3461
  • https://www.jianshu.com/p/1dc4250c6f6f
  • https://ruby-china.org/topics/38429(一个 MySQL 死锁案例分析 --Index merge when update)
  • https://yq.aliyun.com/articles/8963/
  • https://www.aneasystone.com/archives/2018/04/solving-dead-locks-four.html
  • http://seanlook.com/2017/03/11/mysql-index_merge-deadlock/
  • http://www.ishenping.com/ArtInfo/133925.html (Innodb死锁日志分段解读-如何阅读死锁日志)
  • https://segmentfault.com/a/1190000018730103 (了解MySQL死锁日志)
  • https://www.itread01.com/content/1546402384.html
  • https://www.jianshu.com/p/e4f87d301415
  • http://loesspie.com/2019/06/12/mysql-using-intersect-deadlock/
  • 《深入浅出Mysql》 第二版

你可能感兴趣的:(记录一次 mysql 死锁 (患者 为 index merge))