MySQL InnoDB存储引擎的一次死锁分析

通过show engine innodb status 可以查看innodb存储引擎的信息,找到“LATEST DETECTED DEADLOCK”这一节可以查看最新的死锁日志

死锁日志:

------------------------

LATEST DETECTED DEADLOCK
------------------------
2018-03-28 09:38:37 2afd3a481700
*** (1) TRANSACTION:
TRANSACTION 6474419271, ACTIVE 0.002 sec setting auto-inc lock
mysql tables in use 45, locked 45
LOCK WAIT 4 lock struct(s), heap size 360, 0 row lock(s), undo log entries 2
LOCK BLOCKING MySQL thread id: 487304824 block 135220653
MySQL thread id 135220653, OS thread handle 0x2afd3d840700, query id 161222108 192.168.1.2 cz checking permissions
load data local infile  '/data/applications/test.5148.bcp' 
                    into table test fields terminated by '\t' lines terminated by '\n'
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
TABLE LOCK table `che`.`test` /* Partition `p_20180328` */ trx id 6474419271 lock mode AUTO-INC waiting
*** (2) TRANSACTION:
TRANSACTION 6474419269, ACTIVE 0.002 sec setting auto-inc lock
mysql tables in use 45, locked 45
8 lock struct(s), heap size 1184, 5 row lock(s), undo log entries 31
MySQL thread id 487304824, OS thread handle 0x2afd3a481700, query id 161222106 192.168.1.2 cz checking permissions
load data local infile  '/data/applications/test.10419.bcp' 
                    into table test fields terminated by '\t' lines terminated by '\n'
*** (2) HOLDS THE LOCK(S):
TABLE LOCK table `chezai`.`t_electricvehiclestateinfo` /* Partition `p_20180328` */ trx id 6474419269 lock mode AUTO-INC
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
TABLE LOCK table `chezai`.`t_electricvehiclestateinfo` /* Partition `p_20180327` */ trx id 6474419269 lock mode AUTO-INC waiting

*** WE ROLL BACK TRANSACTION (1)

从以上死锁日志可以看出引起死锁的sql是load data local infile  '/data/applications/test.5148.bcp' 

                    into table test fields terminated by '\t' lines terminated by '\n'

sql是批量将数据插入到表 test中,这个表由于数据量比较大,按时间进行了分区

日志描述了,事务1尝试获取20180328分区的自增锁AUTO-INC,事务2持有20180328分区自增锁AUTO-INC,尝试申请20180327分区自增锁,可以猜测事务1持有20180327自增锁,不然不是死锁了。

--------补充知识开始--------

补充知识:

在InnoDB表中,若存在自增字段,则会维护一个表级别的锁,这里称为自增锁。每次插入新数据,或者update语句修改了此字段,都会需要获取这个锁,由于一个事务可能包含多个语句,而并非所有的语句都与自增字段有关,因此InnoDB作了一个特殊的处理,自增锁在一个语句结束后马上被释放。之所以说是特殊处理,是因为普通的锁,都是在事务结束后释放。

与这个自增锁相关的一个参数是innodb_autoinc_lock_mode. 默认值为1,可选为0,1,2。
    我们先来看当这个值设置为0时,一个有自增字段的表,插入一行数据时的行为:
    1) 申请AUTO_INC锁
    2) 得到当前AUTO_INCREMNT值n,给AUTO_INCREMENT 加1
    3) 执行插入操作,并将n填入新增的行对应字段中
    4) 释放AUTO_INC锁
我们看到这个过程中,虽然InnoDB为了减少锁粒度,在语句执行完成就马上释放,但这锁还是太大了――它包括了插入操作的时间。这就导致了两个insert语句,实际上没办法并行
很容易想到设置为1的时候,应该是将3) 和 4)对调。但是本文还是要讨论为0的情况,因为我们的前提是LOAD语句,而LOAD语句这类插入多行的语句中(包括insert …select …),即使设置为1也没用,会退化为0的模式。

    之所以会退化为0模式是为了确保主从一致性:

设想binlog_format='statement',一个LOAD DATA语句在主库的binlog直接记录为语句本身,那从库如何重放:
1) 将load data用到的文件发给slave,slave将文件保存在临时目录。
2) 在slave也执行一次LOAD DATA语句。
其间有一个问题:slave怎么保证load data语句的自增id字段与master相同?

为了解决这个问题,主库的binlog中还有一个set SET INSERT_ID命令,表明这个LOAD DATA语句插入的第一行的自增ID值。这样slave在执行load data之前,先执行了这个set SET INSERT_ID语句,用于保证执行结果与主库一模一样。

在确保初始ID相同的情况下:

如果innodb_autoinc_lock_mode为模式1,也就是上文中3),4)步骤对调,即获取下一个值以后就就释放锁,再需要插入下一条数据的时候再申请(此时可能有其他事物获得锁并更改了自增值),此时就无法保证这次批量操作的数据的自增ID是连续的。在从库中没有主库的类似情景,在执行批量操作的时候会是连续的自增ID,此时主从不一致。

如果innodb_autoinc_lock_mode为模式0,批量操作的流程如下:

1) 插入第一条数据
2) 申请AUTO_INC锁
3) 插入第二条
4) 申请AUTO_INC 锁(因为已经是自己的,直接成功)
5) 。。。。。。插入剩余所有行

6) 释放AUTO_INC锁。

所以这个流程就简单描述为:插入第一行,申请AUTO_INC锁,然后插入剩下的所有行后再释放。

此时主库中批量操作的数据自增值是连续的,主从一致。

--------补充知识结束--------

在回到上述的死锁问题,解决办法:

(1)前面我们说到innodb_autoinc_lock_mode这个参数的可选值有0、1、2。当设置为1的时候,在LOAD DATA语句会退化为模式0。但若设置为2,则无论如何都会使用模式1。我们前面说到使用模式1会导致LOAD DATA生成的自增id值不连续,这样会导致在binlog_format是1时主从不一致,因此设置为2的前提,是binlog_format 是row.在binlog_format='row'时,设置innodb_autoinc_lock_mode为2是安全的。

如果一直为模式1,在批量操作中,自增锁将会被更快的释放,即load data语句会更快的释放分区的自增锁,这样将会降低死锁发生的概率。

(2)细化分区,增加子分区,细化分区自增锁的粒度,降低发生死锁的概率;

(3)更改导入数据文件,确保一条批量导入的语句不出现夸分区插入数据的情况,根本性消除出现死锁的条件。

在条件合适的情况下,建议按照方法(1)做优化,可以提高总体的插入数据的性能,针对现在的情况方法(3)是“治本”的方案


小弟才疏学浅,如果有理解错误的地方欢迎批评指正,不吝赐教!

你可能感兴趣的:(MySQL InnoDB存储引擎的一次死锁分析)