InnoDB的锁机制 | wsztrush
在两个及以上的事务在执行过程中,可能因为争夺资源而互相等待:
- T1拥有资源A,尝试获取资源B;
- T2拥有资源B,尝试获取资源A;
如果不暴力终结某个事务,T1、T2会永远等下去,这就是死锁。数据库通过wait-for graph(等待图)来检测是否存在死锁,当发现有回路时,引擎会选择回滚undo量最小的事务。
程序上为防止死锁一般会统一更新顺序。
加锁分析
下面对常见的几条SQL做加锁分析,建表如下:
1 2 3 4 5 6 7 8 9 10 | CREATE TABLE `my_test` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `a` bigint(20) NOT NULL, `b` bigint(20) NOT NULL, `c` bigint(20) NOT NULL, `d` bigint(20) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `unique_a_b`(`a`,`b`), KEY `idx_c`(`c`) ); |
初始化数据如下:

在执行完成SQL后,可以从information_schema中的:
- INNODB_TRX
- INNODB_LOCKS
- INNODB_LOCK_WAITS
来查看锁和事务的状态,此外也可以用show engine innodb status来看整体情况,需要增加锁监控:
1 | create table innodb_lock_monitor(x int) engine=innodb; |
才可以看到比较详细的锁信息。
RC+主键
1 | select * from my_test where id = 1 for update; |
加锁信息:
1 2 3 4 5 | 2 lock struct(s), heap size 360, 1 row lock(s) MySQL thread id 2, OS thread handle 0x1648, query id 148 localhost 127.0.0.1 root cleaning up TABLE LOCK table `mysql`.`my_test` trx id 2863 lock mode IX RECORD LOCKS space id 38 page no 3 n bits 72 index `PRIMARY` of table `mysql`.`my_test` trx id 2863 lock_mode X locks rec but not gap Record lock, heap no 2 PHYSICAL RECORD: n_fields 7; compact format; info bits 0 |
加锁逻辑为:
- 表上加IX锁;
- 行上加X锁;
RC+唯一约束
1 | select * from my_test where a = 1 and b = 1 for update; |
加锁信息如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 3 lock struct(s), heap size 360, 2 row lock(s) MySQL thread id 2, OS thread handle 0x1648, query id 152 localhost 127.0.0.1 root cleaning up TABLE LOCK table `mysql`.`my_test` trx id 2864 lock mode IX RECORD LOCKS space id 38 page no 4 n bits 72 index `unique_a_b` of table `mysql`.`my_test` trx id 2864 lock_mode X locks rec but not gap Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0 0: len 8; hex 8000000000000001; asc ;; 1: len 8; hex 8000000000000001; asc ;; 2: len 8; hex 8000000000000001; asc ;; RECORD LOCKS space id 38 page no 3 n bits 72 index `PRIMARY` of table `mysql`.`my_test` trx id 2864 lock_mode X locks rec but not gap Record lock, heap no 2 PHYSICAL RECORD: n_fields 7; compact format; info bits 0 0: len 8; hex 8000000000000001; asc ;; 1: len 6; hex 000000000b23; asc #;; 2: len 7; hex 9c000001500110; asc P ;; 3: len 8; hex 8000000000000001; asc ;; 4: len 8; hex 8000000000000001; asc ;; 5: len 8; hex 8000000000000001; asc ;; 6: len 8; hex 8000000000000001; asc ;; |
加锁逻辑为:
- 表上加IX锁;
- 唯一约束上加X锁;
- 行上加X锁;
RC+普通索引
1 | select * from my_test where c = 3 for update; |
加锁信息如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | 3 lock struct(s), heap size 360, 5 row lock(s) MySQL thread id 2, OS thread handle 0x1648, query id 166 localhost 127.0.0.1 root cleaning up TABLE LOCK table `mysql`.`my_test` trx id 2872 lock mode IX RECORD LOCKS space id 38 page no 5 n bits 72 index `idx_c` of table `mysql`.`my_test` trx id 2872 lock_mode X Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0 0: len 8; hex 73757072656d756d; asc supremum;; Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 0 0: len 8; hex 8000000000000003; asc ;; 1: len 8; hex 8000000000000003; asc ;; Record lock, heap no 5 PHYSICAL RECORD: n_fields 2; compact format; info bits 0 0: len 8; hex 8000000000000003; asc ;; 1: len 8; hex 8000000000000004; asc ;; RECORD LOCKS space id 38 page no 3 n bits 72 index `PRIMARY` of table `mysql`.`my_test` trx id 2872 lock_mode X locks rec but not gap Record lock, heap no 4 PHYSICAL RECORD: n_fields 7; compact format; info bits 0 0: len 8; hex 8000000000000003; asc ;; 1: len 6; hex 000000000b23; asc #;; 2: len 7; hex 9c000001500130; asc P 0;; 3: len 8; hex 8000000000000003; asc ;; 4: len 8; hex 8000000000000003; asc ;; 5: len 8; hex 8000000000000003; asc ;; 6: len 8; hex 8000000000000003; asc ;; Record lock, heap no 5 PHYSICAL RECORD: n_fields 7; compact format; info bits 0 0: len 8; hex 8000000000000004; asc ;; 1: len 6; hex 000000000b37; asc 7;; 2: len 7; hex ac0000015e0110; asc ^ ;; 3: len 8; hex 8000000000000004; asc ;; 4: len 8; hex 8000000000000004; asc ;; 5: len 8; hex 8000000000000003; asc ;; 6: len 8; hex 8000000000000004; asc ;; |
加锁逻辑为:
- 表上加IX锁;
- 索引上加X锁(2);
- 行上加X锁(2);
RC+无索引
1 | select * from my_test where d = 1 for update; |
加锁信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | 2 lock struct(s), heap size 360, 5 row lock(s) MySQL thread id 2, OS thread handle 0x1648, query id 172 localhost 127.0.0.1 root cleaning up TABLE LOCK table `mysql`.`my_test` trx id 2874 lock mode IX RECORD LOCKS space id 38 page no 3 n bits 72 index `PRIMARY` of table `mysql`.`my_test` trx id 2874 lock_mode X Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0 0: len 8; hex 73757072656d756d; asc supremum;; Record lock, heap no 2 PHYSICAL RECORD: n_fields 7; compact format; info bits 0 0: len 8; hex 8000000000000001; asc ;; 1: len 6; hex 000000000b23; asc #;; 2: len 7; hex 9c000001500110; asc P ;; 3: len 8; hex 8000000000000001; asc ;; 4: len 8; hex 8000000000000001; asc ;; 5: len 8; hex 8000000000000001; asc ;; 6: len 8; hex 8000000000000001; asc ;; Record lock, heap no 3 PHYSICAL RECORD: n_fields 7; compact format; info bits 0 0: len 8; hex 8000000000000002; asc ;; 1: len 6; hex 000000000b23; asc #;; 2: len 7; hex 9c000001500120; asc P ;; 3: len 8; hex 8000000000000002; asc ;; 4: len 8; hex 8000000000000002; asc ;; 5: len 8; hex 8000000000000002; asc ;; 6: len 8; hex 8000000000000002; asc ;; Record lock, heap no 4 PHYSICAL RECORD: n_fields 7; compact format; info bits 0 0: len 8; hex 8000000000000003; asc ;; 1: len 6; hex 000000000b23; asc #;; 2: len 7; hex 9c000001500130; asc P 0;; 3: len 8; hex 8000000000000003; asc ;; 4: len 8; hex 8000000000000003; asc ;; 5: len 8; hex 8000000000000003; asc ;; 6: len 8; hex 8000000000000003; asc ;; Record lock, heap no 5 PHYSICAL RECORD: n_fields 7; compact format; info bits 0 0: len 8; hex 8000000000000004; asc ;; 1: len 6; hex 000000000b37; asc 7;; 2: len 7; hex ac0000015e0110; asc ^ ;; 3: len 8; hex 8000000000000004; asc ;; 4: len 8; hex 8000000000000004; asc ;; 5: len 8; hex 8000000000000003; asc ;; 6: len 8; hex 8000000000000004; asc ;; |
加锁逻辑:
- 表上加X锁;
- 行上加X锁(4);
RR
在事务1中执行:
1 | delete from my_test where c = 3; |
在事务2中执行:
1 | insert into my_test values(6, 6, 6, 3, 6); |
查看事务2的锁信息如下:
1 2 3 4 5 6 7 8 | mysql tables in use 1, locked 1 LOCK WAIT 2 lock struct(s), heap size 360, 1 row lock(s), undo log entries 1 MySQL thread id 3, OS thread handle 0x2378, query id 198 localhost 127.0.0.1 root update insert into my_test values(6, 6, 6, 3, 6) ------- TRX HAS BEEN WAITING 20 SEC FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 38 page no 5 n bits 72 index `idx_c` of table `mysql`.`my_test` trx id 2897 lock_mode X insert intention waiting Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0 0: len 8; hex 73757072656d756d; asc supremum;; |
查看锁等待信息select * from information_schema.innodb_locks如下:

而对于lock_data:
If a gap lock is taken for key values or ranges above the largest value in the index, LOCK_DATA reports “supremum pseudo-record”.
根据上面的Next-Key算法分析,锁定的不只是单个记录,而是一个范围:[3, 4)。
死锁分析
首先模拟常见的死锁,事务1:
1 2 | begin; select * from my_test where id = 1 for update; |
事务2:
1 2 | begin; select * from my_test where id = 2 for update; |
事务1:
1 | select * from my_test where id = 2 for update; |
此时处于锁等待,接着事务2:
1 | select * from my_test where id = 1 for update; |
此时事务2会报死锁,然后将事务回滚,查看死锁信息如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | ------------------------ LATEST DETECTED DEADLOCK ------------------------ 2016-11-22 21:45:37 2378 *** (1) TRANSACTION: TRANSACTION 2899, ACTIVE 48 sec starting index read mysql tables in use 1, locked 1 LOCK WAIT 3 lock struct(s), heap size 360, 2 row lock(s) MySQL thread id 2, OS thread handle 0x1648, query id 217 localhost 127.0.0.1 root statistics select * from my_test where id = 2 for update *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 38 page no 3 n bits 80 index `PRIMARY` of table `mysql`.`my_test` trx id 2899 lock_mode X locks rec but not gap waiting Record lock, heap no 3 PHYSICAL RECORD: n_fields 7; compact format; info bits 0 0: len 8; hex 8000000000000002; asc ;; 1: len 6; hex 000000000b23; asc #;; 2: len 7; hex 9c000001500120; asc P ;; 3: len 8; hex 8000000000000002; asc ;; 4: len 8; hex 8000000000000002; asc ;; 5: len 8; hex 8000000000000002; asc ;; 6: len 8; hex 8000000000000002; asc ;; *** (2) TRANSACTION: TRANSACTION 2900, ACTIVE 28 sec starting index read, thread declared inside InnoDB 5000 mysql tables in use 1, locked 1 3 lock struct(s), heap size 360, 2 row lock(s) MySQL thread id 3, OS thread handle 0x2378, query id 218 localhost 127.0.0.1 root statistics select * from my_test where id = 1 for update *** (2) HOLDS THE LOCK(S): RECORD LOCKS space id 38 page no 3 n bits 80 index `PRIMARY` of table `mysql`.`my_test` trx id 2900 lock_mode X locks rec but not gap Record lock, heap no 3 PHYSICAL RECORD: n_fields 7; compact format; info bits 0 0: len 8; hex 8000000000000002; asc ;; 1: len 6; hex 000000000b23; asc #;; 2: len 7; hex 9c000001500120; asc P ;; 3: len 8; hex 8000000000000002; asc ;; 4: len 8; hex 8000000000000002; asc ;; 5: len 8; hex 8000000000000002; asc ;; 6: len 8; hex 8000000000000002; asc ;; *** (2) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 38 page no 3 n bits 80 index `PRIMARY` of table `mysql`.`my_test` trx id 2900 lock_mode X locks rec but not gap waiting Record lock, heap no 2 PHYSICAL RECORD: n_fields 7; compact format; info bits 0 0: len 8; hex 8000000000000001; asc ;; 1: len 6; hex 000000000b23; asc #;; 2: len 7; hex 9c000001500110; asc P ;; 3: len 8; hex 8000000000000001; asc ;; 4: len 8; hex 8000000000000001; asc ;; 5: len 8; hex 8000000000000001; asc ;; 6: len 8; hex 8000000000000001; asc ;; *** WE ROLL BACK TRANSACTION (2) |
从死锁信息上能看到事务分别拥有哪些锁、在申请哪些锁的时候互相等待了。
INSERT + Unique Key
还是沿用上面的表,开启事务1:
1 2 | begin; insert into my_test values (7, 7, 7, 0, 0); |
开启事务2:
1 2 | begin; insert into my_test values (8, 7, 7, 0, 0); |
此时会锁等待,再开启事务3:
1 2 | begin; insert into my_test values (9, 7, 7, 0, 0); |
此时的锁情况为:

原因:
事务1在插入是需要加X锁,而事务2、事务3在插入前需要做唯一性检查,所以需要加S锁,因为与X锁不兼容所以事务2、3处于等待状态。
接着事务1进行rollback话,事务2会插入成功,事务3会报死锁:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | 2016-11-22 22:36:36 1ea4 *** (1) TRANSACTION: TRANSACTION 2904, ACTIVE 300 sec inserting mysql tables in use 1, locked 1 LOCK WAIT 4 lock struct(s), heap size 1184, 2 row lock(s), undo log entries 1 MySQL thread id 3, OS thread handle 0x2378, query id 238 localhost 127.0.0.1 root update insert into my_test values (8, 7, 7, 0, 0) *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 38 page no 4 n bits 72 index `unique_a_b` of table `mysql`.`my_test` trx id 2904 lock_mode X insert intention waiting Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0 0: len 8; hex 73757072656d756d; asc supremum;; *** (2) TRANSACTION: TRANSACTION 2905, ACTIVE 290 sec inserting, thread declared inside InnoDB 1 mysql tables in use 1, locked 1 4 lock struct(s), heap size 1184, 2 row lock(s), undo log entries 1 MySQL thread id 6, OS thread handle 0x1ea4, query id 239 localhost 127.0.0.1 root update insert into my_test values (9, 7, 7, 0, 0) *** (2) HOLDS THE LOCK(S): RECORD LOCKS space id 38 page no 4 n bits 72 index `unique_a_b` of table `mysql`.`my_test` trx id 2905 lock mode S Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0 0: len 8; hex 73757072656d756d; asc supremum;; *** (2) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 38 page no 4 n bits 72 index `unique_a_b` of table `mysql`.`my_test` trx id 2905 lock_mode X insert intention waiting Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0 0: len 8; hex 73757072656d756d; asc supremum;; *** WE ROLL BACK TRANSACTION (2) |
原因:
在事务1回滚后,事务2、3都获得了S锁,接着要执行INSERT要去获取X锁,事务2申请X锁时发现与事务3的S锁不兼容,事务3也是同样的情况,于是出现死锁。
特殊的唯一约束:主键也有相同的问题。
总结
对InnoDB了解的还是很少,最近把锁相关的突击了一下,抽时间把底层的实现也了解一下,其他部分再慢慢补上。