性能文章>大招落地:MySQL 插入更新死锁源码分析>

大招落地:MySQL 插入更新死锁源码分析原创

7311310

天再来分析一个死锁场景。下面开始真正的内容。

建表语句:

CREATE TABLE `tenant_config` (
  `id` bigint(21) NOT NULL AUTO_INCREMENT,
  `tenant_id` int(11) NOT NULL,
  `open_card_point` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uidx_tenant` (`tenant_id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8mb4

表中有一条初始化数据:

INSERT INTO `tenant_config` (`tenant_id`, `open_card_point`) VALUES (123,0);

数据库隔离级别:RC

两条 insert,两条 update

事务 1 和事务 2 语句一毛一样,都是下面这样:

INSERT INTO `tenant_config` ( `tenant_id`, `open_card_point`) VALUES (123,111111);

UPDATE tenant_config SET open_card_point =  0 where tenant_id = 123;

代码的逻辑大概如下,先插入,如果有冲突则更新

try {
    insert();
} catch (DuplicateKeyException e) {
    update()
}

死锁条件的过程如下

事务 1:

INSERT INTO `tenant_config` ( `tenant_id`, `open_card_point`) VALUES (123,111111);
ERROR 1062 (23000): Duplicate entry '123' for key 'uidx_tenant'

加锁情况,对 uk 加 S 锁,如下:

image.png

事务 2:

INSERT INTO `tenant_config` ( `tenant_id`, `open_card_point`) VALUES (123,111111);
ERROR 1062 (23000): Duplicate entry '123' for key 'uidx_tenant'

加锁情况,对 uk 加 S 锁,如下:

image.png

事务 1:

UPDATE tenant_config SET open_card_point =  0 where tenant_id = 123;

对 uk 加 X 锁,因为事务 2 获取了 S 锁,进入锁等待

image.png

事务 2:

UPDATE tenant_config SET open_card_point =  0 where tenant_id = 123;

同样想对 uk 加 X 锁,死锁条件产生:事务 2 拿到了 S 锁,想加 X 锁,事务 1 拿到了 S 锁,也想加 X 锁,彼此都在等对方的 S 锁。

image.png

这种情况是最简单的,如果只是这么简单,我就不会写了,哈哈,下面来看第二种情况。

一条 insert,两条 update

第一步:事务 1,插入唯一键冲突

begin;
INSERT INTO `tenant_config` ( `tenant_id`, `open_card_point`) VALUES (123,111111);

ERROR 1062 (23000): Duplicate entry '123' for key 'uidx_tenant'

第二步:事务 2

begin;
UPDATE tenant_config SET open_card_point =  0 where tenant_id = 123 and 1 =1;

第三步:事务 1

UPDATE tenant_config SET open_card_point =  0 where tenant_id = 123 and 1 =1;

出现:事务 2 死锁

ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

分析过程如下:

事务 1

INSERT INTO `tenant_config` ( `tenant_id`, `open_card_point`) VALUES (123,111111);

对 uk 加 S 锁,这个没有什么歧义。

image.png

接下来事务 2

UPDATE tenant_config SET open_card_point =  0 where tenant_id = 123 and 1 =1;

这个对 ux 加 X 锁,进入锁等待状态,这个也没有什么问题。

image.png

接下来,事务 1 执行 update,情况就复杂很多了,也是想获取 X 锁,但是没有那么顺利。

进入死锁检测流程,重点代码在lock_deadlock_occurs()函数,最近会进入 lock_deadlock_recursive()递归调用函数。

image.png

  • start 表示顶层调用该函数的事务指针,比如现在正在执行的事务 1 就是 start

  • wait_lock 表示想要获取的锁,这里是事务 1 对 uk 的 X 锁。

  • trx 等待锁的事务指针

死锁的本质是:在递归过程中,如果冲突出现的锁事务id等于顶层事务id(lock_trx == start),则说明有环,就发生死锁。

image.png

以下记事务 1 为 t1,事务 2 为 t2

第一次递归

wait_lock 属于 t1 的 lock_X,就是 t1 update 想获取的 X 锁

image.png

这个时候会检查记录上所有的锁,第一个锁是 t1 事务的 S 锁,第二个锁是 t2 事务等待状态的 X 锁

检查第一把锁,t1 事务的 S 锁,因为与 wait_lock 属于同一个事务,没有冲突,继续检查第二把锁。

image.png

检查第二把锁,是 t2 事务处于等待状态的 X 锁,是互斥的,而且 t2 的 X 锁是处于等待状态的,开始第二次递归调用,检查 t2 的 X 锁,查看它在等待什么锁。

image.png

第二次递归

此时传入的 start 没变,wait_lock 变为了 t2 的 X 锁,也就是把 t2 的 X 锁拿出来检测,看跟现有锁有哪些依赖。

t2 的 X 锁在等待 t1 的 S 锁,lock_trx 等于 start,成环死锁产生。

image.png

也就是:t1 的 insert 插入加了 S 锁,t2 的 X 锁虽然没加成功,但是真实存在,标记为等待状态。t1 再想获取 X 锁,发现与 t2 等待状态的 X 锁冲突。再次检测,发现 t2 等待状态的 X 锁与 t1 的 S 锁冲突,死锁产生。

我画了一个图方便你理解:

image.png

后记

死锁分析是比较复杂的,调试源码可以比较清晰的理清思路,上面是我调试源码的一些结论,如果有理解有误的地方,记得及时帮我指出。

请先登录,再评论

在mysql 的文档上有这个死锁示例,明确表示主键冲突情况下会加s锁https://dev.mysql.com/doc/refman/5.7/en/innodb-locks-set.html
image.png

1年前

个人觉得要讨论MySQL锁事先要知道加锁原理,比如唯一索引/非唯一索引在 RC/RR 加锁原理是不一样,如果一上来就分析源码很容易把自己搞糊涂

11年前
回复 spring_430960:

学习

1年前回复
回复 spring_430960:

这个是编辑摘取了其中一篇文章。

1年前回复

RC隔离模式下,insert into导致的死锁还是很多的,不同mysql版本表现还不一样。如唯一索引下,insert
Into不同版本不一定有间隙锁

21年前

为你推荐

类初始化导致死锁
一张图简单描述死锁 如上图,Thread1 拿到了 object1,Thread2 拿到了 object2,但是现在 Thread1 需要拿到 object2 的锁才能继续往下,Thread2 又要拿到 object1 才能继续往下
Java语言
消失的死锁
问题描述如果java层面发生了死锁,当我们使用jstack命令的时候其实是可以将死锁的信息给dump出来的,在dump结果的最后会有类似Found one Java-level deadlock:的关
在调试里看 NV 驱动栈溢出导致的连环死锁
最近我使用的一台PC随机出现应用程序卡死。卡死的程序可能是VirtualBox虚拟机,可能是资源管理器,也可能是其它软件。有时是一个程序卡死,有时是几个程序卡死,甚至是卡死一大片,整个系统不能动弹,只
通过生产者与消费者模型感受死锁
一. 实验目的及实验环境 1.实验目的通过观察、分析实验现象,深入理解产生死锁的原因,学会分析死锁的方法, 并利用 pstack、 gdb 或 core 文件分析( valgrind (DRD+Hel
记一次中间件导致的慢SQL排查过程
前言最近发现线上出现一个奇葩的问题,这问题让笔者定位了好长时间,期间排查问题的过程还是挺有意思的,正好博客也好久不更新了,就以此为素材写出了本篇文章。 Bug现场我们的分库分表中间件在经过一年的沉淀之
MySQL 死锁套路:一次诡异的批量插入死锁问题分析
线上最近出现了批量insert的死锁,百思不得解。死锁记录如下:```2018-10-26T11:04:41.759589Z 8530809 [Note] InnoDB: (1) TRANSACTI
MySQL 死锁套路:唯一索引下批量插入顺序不一致
死锁的本质是资源竞争,批量插入如果顺序不一致很容易导致死锁,我们来分析一下这个情况。为了方便演示,把批量插入改写为了多条 insert。先来做几个小实验,简化的表结构如下:```CREATE TABL
MySQL 死锁套路:再来看一例走不同索引更新的例子
前面有文章介绍了利用调试MySQL源码的方式来调试锁相关的信息,这里利用这个工具来解决一个比较简单的问题,线上的表字段较多,这里简单成为了一个表:```CREATE TABLE `t3` ( `id