参考文章

《记录一次MySQL死锁的分析与解决过程》

《mysql之show engine innodb status解读》

《把MySQL中的各种锁及其原理都画出来》

        写在开头:本文为学习后的总结,可能有不到位的地方,错误的地方,欢迎各位指正。

前言

        在之前的文章(《mysql运维脚本与个人理解》)中介绍了Mysql运维的几个简单脚本。

但最后的死锁根因分析因为缺少实践经验与分析文章搁置了,这里来补上。

目录

前言

        一、行锁的实现

        二、show engine innodb status的介绍与使用

        1、内容介绍        

        2、内容分析

        3、解决办法


一、行锁的实现

        在《mysql之事务、锁、隔离级别与MVCC》一文中,介绍了行锁是基于索引的,只有在命中索引时才会出现行锁。但实际上并没有解释清楚行锁是怎么做到精确锁住每一行数据的,也因此带来了一些疑问,比如:2个单列索引,假设事务A通过索引1获取了行锁,既然是加在索引上的锁,那也就意味着,只是锁住了索引1,而索引2上是没有限制的,是否意味着事务2可以通过索引2改动这一行的数据?

        要回答这个问题,我们首先回顾下《mysql库中索引的基础概念》中介绍的InnoDB的索引物理结构,二级索引通过主键索引访问数据。

        

mysql show warnings没效果 mysql show engine_MySQL

        重点来了,行锁实际上也会判断命中的索引是否为主键,如不是,则会在该命中的索引以及主键索引上加锁,如下图所示。因此,开头提到的问题自然是无法修改的,因为最后的最后有主键索引把关。

mysql show warnings没效果 mysql show engine_sql_02

        (图片来源《把MySQL中的各种锁及其原理都画出来》

二、show engine innodb status的介绍与使用

        很多时候我们的死锁出现后立马就会回滚,无法通过报错日志直接定位到异常的sql语句,这里就需要使用mysql为我们提供的分析工具show engine innodb status

        1、内容介绍        

        下面是前几天刚遇到的问题,正好拿来做介绍。

------------------------
LATEST DETECTED DEADLOCK
------------------------
2021-08-18 14:04:16 0x7f29f2ef5700
*** (1) TRANSACTION:
TRANSACTION 14235673, ACTIVE 0 sec starting index read
mysql tables in use 3, locked 3
LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 10089425, OS thread handle 139823504013056, query id 157595766 10.75.34.61 dbroot updating
update table1 set aaa='4',bbb='121',ccc='合格',ddd='0',eee='' where bd='6f174b50-8d32' and dw='1e0adeed-3f0d-450a' and tag =1
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 14096 page no 57 n bits 272 index bd_index of table `mydatabase`.`table1` trx id 14235673 lock_mode X locks rec but not gap waiting
Record lock, heap no 114 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 30; hex 36663137346235302d386433322d343763362d613062622d633962333238; asc 6f174b50-8d32-47c6-a0bb-c9b328; (total 36 bytes);
 1: len 30; hex 30636538343031652d653061662d343733362d623031372d626464623663; asc 0ce8401e-e0af-4736-b017-bddb6c; (total 36 bytes);

*** (2) TRANSACTION:
TRANSACTION 14235674, ACTIVE 0 sec fetching rows
mysql tables in use 3, locked 3
9 lock struct(s), heap size 1136, 6 row lock(s), undo log entries 2
MySQL thread id 10089424, OS thread handle 139818146158336, query id 157595768 10.75.34.61 dbroot updating
update table1 set aaa='10',bbb='12',ccc='合格',ddd='0',eee='' where bd='6f174b50-8d32' and dw='ffb27cdc-ba40-4e16' and tag =1
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 14096 page no 57 n bits 272 index bd_index of table `mydatabase`.`table1` trx id 14235674 lock_mode X locks rec but not gap
Record lock, heap no 114 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 30; hex 36663137346235302d386433322d343763362d613062622d633962333238; asc 6f174b50-8d32-47c6-a0bb-c9b328; (total 36 bytes);
 1: len 30; hex 30636538343031652d653061662d343733362d623031372d626464623663; asc 0ce8401e-e0af-4736-b017-bddb6c; (total 36 bytes);

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 14096 page no 57 n bits 272 index bd_index of table `mydatabase`.`table1` trx id 14235674 lock_mode X locks rec but not gap waiting
Record lock, heap no 105 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 30; hex 36663137346235302d386433322d343763362d613062622d633962333238; asc 6f174b50-8d32-47c6-a0bb-c9b328; (total 36 bytes);
 1: len 30; hex 66396262333533362d356137342d343633352d386335632d323666356537; asc f9bb3536-5a74-4635-8c5c-26f5e7; (total 36 bytes);

*** WE ROLL BACK TRANSACTION (1)

        TRANSACTION 14235673, ACTIVE 0 sec starting index read   

        事务14235673,ACTIVE 0 sec表示事务处于活跃状态0s,starting index read表示正在使用索引读取数据行 

        mysql tables in use 3, locked 3

        事务1正在使用3个表,且涉及锁的表有3个

        LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1

        这行表示在等待3把锁,占用内存1136字节,涉及2行记录,如果事务已经锁定了几行数据。

        MySQL thread id 10089425, OS thread handle 139823504013056, query id 157595766 10.75.34.61 dbroot updating

        该事务的线程ID信息,操作系统句柄信息,连接来源、用户等

        update table1 set aaa='4',bbb='121',ccc='合格',ddd='0',eee='' where bd='6f174b50-8d32' and dw='1e0adeed-3f0d-450a' and tag =1

        正在等待行锁的sql

        *** (1) WAITING FOR THIS LOCK TO BE GRANTED:

        *** (2) HOLDS THE LOCK(S):

        正在等待的锁、目前保存的锁

        RECORD LOCKS space id 14096 page no 57 n bits 272 index bd_index of table `mydatabase`.`table1` trx id 14235673 lock_mode X locks rec but not gap waiting

        等待的锁是一个record lock,空间id是14096,页编号为57,大概位置在页的272位处,锁发生在表mydatabase.table1的bd_index 索引上,是一个X锁,但是不是gap lock。 waiting表示正在等待锁

        Record lock, heap no 114 PHYSICAL RECORD: n_fields 2; compact format; info bits 0

        这行表示record lock的heap no 位置(可以用来对照事务2控制住的锁)

        *** WE ROLL BACK TRANSACTION (1)

        回滚了事务1

        2、内容分析

mysql show warnings没效果 mysql show engine_MySQL_03

通过观察事务1等待的行锁在堆中的位置,与事务2获取到的锁在堆中的位置,确定了事务2手中有事务1想要获取的锁。再来分析涉及到的sql,我们发现产生冲突的是同1条sql,在table1这张表中,bd与dw是一对多的关系(这里假设是1:10),也就导致要获取bd_index(基于bd的单列索引)时,要获取到全部的10个bd上的索引,也就导致产生了争用。

        3、解决办法

        问题的原因在于单列索引扫描的范围过大,要获取到全部10个db上的索引,那么我们只要减小获取锁的范围就好了,这里就到了我们的组合索引展示优势的地方了。我们建立bd,dw上的组合索引,这样一来,事务1和事务2中的2条sql都只要获取到自身涉及到的那1条组合索引即可。

create index complex_index on table1(bd,dw);