写点什么

为什么你的 insert 就死锁了

  • 2021 年 11 月 11 日
  • 本文字数:7307 字

    阅读完需:约 24 分钟

事务在请求 S 锁和 X 锁前,需要先获得对应的 IS、IX 锁。


Before a transaction can acquire an S lock on a row in table t, it must first acquire an IS or stronger lock on t. Before a transaction can acquire an X lock on a row, it must first acquire an IX lock on t.


意向锁产生的主要目的是为了处理行锁和表锁之间的冲突,用于表明“某个事务正在某一行上持有了锁,或者准备去持有锁”。


The main purpose of IX and IS locks is to show that someone is locking a row, or going to lock a row in the table.


共享锁、排他锁与意向锁的兼容矩阵如下:


思考

从官方文档字面意思上看意向锁是表级锁,但是大牛不认为“Intention lock 是表级锁”<sup>5</sup>? 另外,由于意向锁主要用于解决行锁与表锁间冲突问题,鉴于平时表级操作特别少,在分析加锁过程是否可以不用过多考虑意向锁的问题?

3. 行锁

记录锁(Record Locks)

记录锁, 仅仅锁住索引记录的一行。 单条索引记录上加锁,record lock 锁住的永远是索引,而非记录本身,即使该表上没有任何索引,那么 innodb 会在后台创建一个隐藏的聚集主键索引,那么锁住的就是这个隐藏的聚集主键索引。所以说当一条 sql 没有走任何索引时,那么将会在每一条聚集索引后面加 X 锁,这个类似于表锁,但原理上和表锁应该是完全不同的。


参见官方文档<sup>3</sup>:


If the table has no PRIMARY KEY or suitable UNIQUE index, InnoDB internally generates a hidden clustered index on a synthetic column containing row ID values. The rows are ordered by the ID that InnoDB assigns to the rows in such a table. The row ID is a 6-byte field that increases monotonically as new rows are inserted. Thus, the rows ordered by the row ID are physically in insertion order.

间隙锁(Gap Locks)

区间锁, 仅仅锁住一个索引区间(开区间)。 在索引记录之间的间隙中加锁,或者是在某一条索引记录之前或者之后加锁,并不包括该索引记录本身。

next-key 锁(Next-Key Locks)

record lock + gap lock, 左开右闭区间。


A next-key lock is a combination of a record lock on the index record and a gap lock on the gap before the index record.

By default, InnoDB operates in REPEATABLE READ transaction isolation level and with the innodb_locks_unsafe_for_binlog system variable disabled. In this case, InnoDB uses next-key locks for searches and index scans, which prevents phantom rows。


默认情况下,innodb 使用 next-key locks 来锁定记录。 但当查询的索引含有唯一属性的时候,Next-Key Lock 会进行优化,将其降级为 Record Lock,即仅锁住索引本身,不是范围。

插入意向锁(Insert Intention Locks)

Gap Lock 中存在一种插入意向锁(Insert Intention Lock),在 insert 操作时产生。在多事务同时写入不同数据至同一索引间隙的时候,并不需要等待其他事务完成,不会发生锁等待。 假设有一个记录索引包含键值 4 和 7,不同的事务分别插入 5 和 6,每个事务都会产生一个加在 4-7 之间的插入意向锁,获取在插入行上的排它锁,但是不会被互相锁住,因为数据行并不冲突。


An insert intention lock is a type of gap lock set by I


【一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义】
浏览器打开:qq.cn.hn/FTf 免费领取
复制代码


NSERT operations prior to row insertion. This lock signals the intent to insert in such a way that multiple transactions inserting into the same index gap need not wait for each other if they are not inserting at the same position within the gap. Suppose that there are index records with values of 4 and 7. Separate transactions that attempt to insert values of 5 and 6, respectively, each lock the gap between 4 and 7 with insert intention locks prior to obtaining the exclusive lock on the inserted row, but do not block each other because the rows are nonconflicting.


注:插入意向锁并非意向锁,而是一种特殊的间隙锁。

4. 行锁的兼容矩阵<sup>4</sup>


表注:横向是已经持有的锁,纵向是正在请求的锁。


由于 S 锁和 S 锁是完全兼容的,因此在判别兼容性时只考虑持有的锁与请求的锁是这三种组合情形:X、S 和 S、X 和 X、X。 另外,需要提醒注意的是进行兼容判断也只是针对于加锁涉及的行有交集的情形。


分析兼容矩阵可以得出如下几个结论:


  • INSERT 操作之间不会有冲突。

  • GAP,Next-Key 会阻止 Insert。

  • GAP 和 Record,Next-Key 不会冲突

  • Record 和 Record、Next-Key 之间相互冲突。

  • 已有的 Insert 锁不阻止任何准备加的锁。

5. 自增锁(AUTO-INC Locks)

AUTO-INC 锁是一种特殊的表级锁,发生涉及 AUTO_INCREMENT 列的事务性插入操作时产生。


官方解释如下<sup>3</sup>:


An AUTO-INC lock is a special table-level lock taken by transactions inserting into tables with AUTO_INCREMENT columns. In the simplest case, if one transaction is inserting values into the table, any other transactions must wait to do their own inserts into that table, so that rows inserted by the first transaction receive consecutive primary key values.

四、insert 加锁过程

官方文档<sup>6</sup>对于 insert 加锁的描述如下:


INSERT sets an exclusive lock on the inserted row. This lock is an index-record lock, not a next-key lock (that is, there is no gap lock) and does not prevent other sessions from inserting into the gap before the inserted row.

Prior to inserting the row, a type of gap lock called an insert intention gap lock is set. This lock signals the intent to insert in such a way that multiple transactions inserting into the same index gap need not wait for each other if they are not inserting at the same position within the gap. Suppose that there are index records with values of 4 and 7. Separate transactions that attempt to insert values of 5 and 6 each lock the gap between 4 and 7 with insert intention locks prior to obtaining the exclusive lock on the inserted row, but do not block each other because the rows are nonconflicting.

If a duplicate-key error occurs, a shared lock on the duplicate index record is set. This use of a shared lock can result in deadlock should there be multiple sessions trying to insert the same row if another session already has an exclusive lock.


简单的 insert 会在 insert 的行对应的索引记录上加一个排它锁,这是一个 record lock,并没有 gap,所以并不会阻塞其他 session 在 gap 间隙里插入记录。


不过在 insert 操作之前,还会加一种锁,官方文档称它为 insertion intention gap lock,也就是意向的 gap 锁。这个意向 gap 锁的作用就是预示着当多事务并发插入相同的 gap 空隙时,只要插入的记录不是 gap 间隙中的相同位置,则无需等待其他 session 就可完成,这样就使得 insert 操作无须加真正的 gap lock。 假设有一个记录索引包含键值 4 和 7,不同的事务分别插入 5 和 6,每个事务都会产生一个加在 4-7 之间的插入意向锁,获取在插入行上的排它锁,但是不会被互相锁住,因为数据行并不冲突。


假设发生了一个唯一键冲突错误,那么将会在重复的索引记录上加读锁。当有多个 session 同时插入相同的行记录时,如果另外一个 session 已经获得该行的排它锁,那么将会导致死锁。

思考:Insert Intention Locks 作用

Insert Intention Locks 的引入,我理解是为了提高数据插入的并发能力。 如果没有 Insert Intention Locks 的话,可能就需要使用 Gap Locks 来代替。

五、insert 死锁场景分析

接下来,带大家看几个与 insert 相关的死锁场景。

1. duplicate key error 引发的死锁

这个场景主要发生在两个以上的事务同时进行唯一键值相同的记录插入操作。

表结构

``CREATE TABLEaa(id int(10) unsigned NOT NULL COMMENT '主键',name varchar(20) NOT NULL DEFAULT '' COMMENT '姓名',age int(11) NOT NULL DEFAULT '0' COMMENT '年龄',stage int(11) NOT NULL DEFAULT '0' COMMENT '关卡数',PRIMARY KEY (id),UNIQUE KEYudx_name(name),KEYidx_stage(stage)) ENGINE=InnoDB DEFAULT CHARSET=utf8;

表数据

``CREATE TABLE aa (id int(10) unsigned NOT NULL COMMENT '主键',name varchar(20) NOT NULL DEFAULT '' COMMENT '姓名',age int(11) NOT NULL DEFAULT '0' COMMENT '年龄',stage int(11) NOT NULL DEFAULT '0' COMMENT '关卡数',PRIMARY KEY (id),UNIQUE KEY udx_name (name),KEY idx_stage (stage)) ENGINE=InnoDB DEFAULT CHARSET=utf8;

事务执行时序表


如果 T1 未 rollback,而是 commit 的话,T2 和 T3 会报唯一键冲突:ERROR 1062 (23000): Duplicate entry ‘6’ for key ‘PRIMARY’

事务锁占用情况

T1 rollback 前,各事务锁占用情况:


mysql> select * from information_schema.innodb_locks;+--------------+-------------+-----------+-----------+-------------+------------+------------+-----------+----------+-----------+| lock_id | lock_trx_id | lock_mode | lock_type | lock_table | lock_index | lock_space | lock_page | lock_rec | lock_data |+--------------+-------------+-----------+-----------+-------------+------------+------------+-----------+----------+-----------+| 36729:24:3:7 | 36729 | S | RECORD | test.aa | PRIMARY | 24 | 3 | 7 | 6 || 36727:24:3:7 | 36727 | X | RECORD | test.aa | PRIMARY | 24 | 3 | 7 | 6 || 36728:24:3:7 | 36728 | S | RECORD | test.aa | PRIMARY | 24 | 3 | 7 | 6 |+--------------+-------------+-----------+-----------+-------------+------------+------------+-----------+----------+-----------+


注:mysql 有自己的一套规则来决定 T2 与 T3 哪个进行回滚,本文不做讨论。

死锁日志


LATEST DETECTED DEADLOCK

2016-07-21 19:34:23 700000a3f000*** (1) TRANSACTION:TRANSACTION 36728, ACTIVE 199 sec insertingmysql tables in use 1, locked 1LOCK WAIT 4 lock struct(s), heap size 1184, 2 row lock(s)MySQL thread id 13, OS thread handle 0x700000b0b000, query id 590 localhost root updateinsert into aa values(6, 'test', 12, 3)*** (1) WAITING FOR THIS LOCK TO BE GRANTED:RECORD LOCKS space id 24 page no 3 n bits 80 index PRIMARY of table test.aa trx id 36728 lock_mode X insert intention waitingRecord lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 00: len 8; hex 73757072656d756d; asc supremum;;*** (2) TRANSACTION:TRANSACTION 36729, ACTIVE 196 sec insertingmysql tables in use 1, locked 14 lock struct(s), heap size 1184, 2 row lock(s)MySQL thread id 14, OS thread handle 0x700000a3f000, query id 591 localhost root updateinsert into aa values(6, 'test', 12, 3)*** (2) HOLDS THE LOCK(S):RECORD LOCKS space id 24 page no 3 n bits 80 index PRIMARY of table test.aa trx id 36729 lock mode SRecord lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 00: len 8; hex 73757072656d756d; asc supremum;;*** (2) WAITING FOR THIS LOCK TO BE GRANTED:RECORD LOCKS space id 24 page no 3 n bits 80 index PRIMARY of table test.aa trx id 36729 lock_mode X insert intention waitingRecord lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 00: len 8; hex 73757072656d756d; asc supremum;;*** WE ROLL BACK TRANSACTION (2)

死锁成因

事务 T1 成功插入记录,并获得索引 id=6 上的排他记录锁(LOCK_X | LOCK_REC_NOT_GAP)。 紧接着事务 T2、T3 也开始插入记录,请求排他插入意向锁(LOCK_X | LOCK_GAP | LOCK_INSERT_INTENTION);但由于发生重复唯一键冲突,各自请求的排他记录锁(LOCK_X | LOCK_REC_NOT_GAP)转成共享记录锁(LOCK_S | LOCK_REC_NOT_GAP)。


T1 回滚释放索引 id=6 上的排他记录锁(LOCK_X | LOCK_REC_NOT_GAP),T2 和 T3 都要请求索引 id=6 上的排他记录锁(LOCK_X | LOCK_REC_NOT_GAP)。 由于 X 锁与 S 锁互斥,T2 和 T3 都等待对方释放 S 锁。 于是,死锁便产生了。


如果此场景下,只有两个事务 T1 与 T2 或者 T1 与 T3,则不会引发如上死锁情况产生。

思考

  • 为什么发现重复主键冲突的时候,要将事务请求的 X 锁转成 S 锁? (比较牵强的)个人理解,跟插入意向锁类型,也是为了提高插入的并发效率。

  • 插入前请求插入意向锁的作用? 个人认为,通过兼容矩阵来分析,Insert Intention Locks 是为了减少插入时的锁冲突。

2. GAP 与 Insert Intention 冲突引发的死锁

表结构

``CREATE TABLE t (a int(11) NOT NULL,b int(11) DEFAULT NULL,PRIMARY KEY (a),KEY idx_b (b)) ENGINE=InnoDB DEFAULT CHARSET=utf8;

表数据

``mysql> select * from t;+----+------+| a | b |+----+------+| 1 | 2 || 2 | 3 || 3 | 4 || 11 | 22 |+----+------+

事务执行时序表

事务锁占用情况

T2 insert 前,各事务锁占用情况:


mysql> select * from information_schema.innodb_locks;+--------------+-------------+-----------+-----------+------------+------------+------------+-----------+----------+-----------+| lock_id | lock_trx_id | lock_mode | lock_type | lock_table | lock_index | lock_space | lock_page | lock_rec | lock_data |+--------------+-------------+-----------+-----------+------------+------------+------------+-----------+----------+-----------+| 36831:25:4:5 | 36831 | X,GAP | RECORD | test.t | idx_b | 25 | 4 | 5 | 22, 11 || 36832:25:4:5 | 36832 | X,GAP | RECORD | test.t | idx_b | 25 | 4 | 5 | 22, 11 |+--------------+-------------+-----------+-----------+------------+------------+------------+-----------+----------+-----------+

死锁日志


LATEST DETECTED DEADLOCK

2016-07-28 12:28:34 700000a3f000*** (1) TRANSACTION:TRANSACTION 36831, ACTIVE 17 sec insertingmysql tables in use 1, locked 1LOCK WAIT 4 lock struct(s), heap size 1184, 3 row lock(s), undo log entries 1MySQL thread id 38, OS thread handle 0x700000b0b000, query id 953 localhost root updateinsert into t values (4,5)*** (1) WAITING FOR THIS LOCK TO BE GRANTED:RECORD LOCKS space id 25 page no 4 n bits 72 index idx_b of table test.t trx id 36831 lock_mode X locks gap before rec insert intention waitingRecord lock, heap no 5 PHYSICAL RECORD: n_fields 2; compact format; info bits 00: len 4; hex 80000016; asc ;;1: len 4; hex 8000000b; asc ;;*** (2) TRANSACTION:TRANSACTION 36832, ACTIVE 13 sec insertingmysql tables in use 1, locked 13 lock struct(s), heap size 360, 2 row lock(s)MySQL thread id 39, OS thread handle 0x700000a3f000, query id 954 localhost root updateinsert into t values (4,5)*** (2) HOLDS THE LOCK(S):RECORD LOCKS space id 25 page no 4 n bits 72 index idx_b of table test.t trx id 36832 lock_mode X locks gap before recRecord lock, heap no 5 PHYSICAL RECORD: n_fields 2; compact format; info bits 00: len 4; hex 80000016; asc ;;1: len 4; hex 8000000b; asc ;;*** (2) WAITING FOR THIS LOCK TO BE GRANTED:RECORD LOCKS space id 25 page no 3 n bits 72 index PRIMARY of table test.t trx id 36832 lock mode S locks rec but not gap waitingRecord lock, heap no 5 PHYSICAL RECORD: n_fields 4; compact format; info bits 00: len 4; hex 80000004; asc ;;1: len 6; hex 000000008fdf; asc ;;2: len 7; hex 8d000001d00110; asc ;;3: len 4; hex 80000005; asc ;;*** WE ROLL BACK TRANSACTION (2)

死锁成因

事务 T1 执行查询语句,在索引 b=6 上加排他 Next-key 锁(LOCK_X | LOCK_ORDINARY),会锁住 idx_b 索引范围(4, 22)。 事务 T2 执行查询语句,在索引 b=8 上加排他 Next-key 锁(LOCK_X | LOCK_ORDINARY),会锁住 idx_b 索引范围(4, 22)。由于请求的 GAP 与已持有的 GAP 是兼容的,因此,事务 T2 在 idx_b 索引范围(4, 22)也能加锁成功。


事务 T1 执行插入语句,会先加排他 Insert Intention 锁。由于请求的 Insert Intention 锁与已有的 GAP 锁不兼容,则事务 T1 等待 T2 释放 GAP 锁。 事务 T2 执行插入语句,也会等待 T1 释放 GAP 锁。 于是,死锁便产生了。


注:LOCK_ORDINARY 拥有 LOCK_GAP 一部分特性。

思考:Insert Intention 锁在加哪级索引上?

这个排他锁加在 PK 上,还是二级索引上?

六、课后思考

  1. 无主键的加锁过程 无 PK 时,会创建一个隐式聚簇索引。加锁在这个隐式聚簇索引会有什么不同?

  2. 复合索引加锁过程

  3. 多条件(where condition)加锁过程

  4. 隐式锁与显式锁,隐式锁什么情况下会转换成显式锁

  5. 如果插入意向锁不阻止任何锁,这个锁还有必要存在吗? 目前看到的作用是,通过加锁的方式来唤醒等待线程。 但这并不意味着,被唤醒后可以直接做插入操作了。需要再次判断是否有锁冲突。

七、补充知识

1. 查看事务隔离级别

SELECT @@global.tx_isolation; SELECT @@session.tx_isolation; SELECT @@tx_isolation;

2. 设置隔离级别

SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE} 例如:set session transaction isolation level read uncommitted;

3. 查看 auto_increment 机制模式

show variables like ‘innodb_autoinc_lock_mode’;

4. 查看表状态

show table status like ‘plan_branch’\G; show table status from test like ‘plan_branch’\G;

评论

发布
暂无评论
为什么你的insert就死锁了