MySQL中由load data语句引起死锁的解决案例

2019-01-04 16:51:40王冬梅

为什么插入多行的语句要即使将innodb_autoinc_lock_mode设置为1,也会用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语句,用于保证执行结果与主库一模一样。

上述的机制能保证主从数据一致的前提是:主从库上LOAD DATA语句生成的自增ID值必须是连续的。

4、背景知识1+2:分析

回到前面说的模式0和1的区别,我们看到,如果AUTO_INC锁在整个语句开始之前就获取,在语句结束之后才释放,这样就能保证整个语句生成的id连续――模式0的保证。

对于1,每次拿到下一个值就释放,插入数据后,若需要再申请,则不连续。

这就是为什么,即使设置为1,对于多行操作,会退化成0。

至此我们知道这个死锁出现的原因,是这两个LOAD DATA语句不仅会访问相同的记录,还会访问同一个AUTO_INC锁,造成互相等待。

到此没完,因为我们知道虽然两个线程访问两个锁可能造成死锁,但是死锁还有另外一个条件,与申请顺序有关。既然AUTO_INC是一个表锁,不论谁先拿到,会阻塞其他同表的LOAD DATA的执行,又为什么会在某个记录上出现锁等待?

5、背景知识3:AUTO_INC的加锁时机

前面我们说到每次涉及到插入新数据,就会要求对AUTO_INC加锁,并列出了流程。但这个流程是对于需要从InnoDB中得到自增值来设置列值的情况。另一种情况是在语句中已经指定了该列的值。

比如对于这个表,执行 insert into tb values(9,100). 此时id的值已经明确是9,虽然不需要取值来填,但是插入这行后有可能需要改变AUTO_INCREMENT的值(若原来是<10,则应该改为10),所以这个锁还是省不了。流程变成:

1) 插入数据

2) 若失败则流程结束

3) 若成功,申请AUTO_INC锁

4) 调用set_max….函数,如有必要则修改AUTO_INCREMENT

5) 语句结束时释放AUTO_INC锁。

6、为什么修改AUTO_INC顺序

这么调整的好处是什么? 主要是为了减少不必要的锁访问。若在插入数据期间发生错误,比如其他字段造成DUPLICATE KEY error,这样就不用访问AUTO_INC锁。

7、死锁过程复现

必须强调是“语句结束时”。这样我们来看一个每行都已经指定了自增列值的LOAD DATA语句的流程(也就是本文例子的情况):