InnoDB 的鎖機制
寫在前面
使用數據庫時,想要較高的吞吐、較低的延遲,但又想在高併發下可以一致地讀寫數據,因此需要高效的鎖機制。
InnoDB中的鎖可以分爲:
- latch:程序上的鎖機制,用來鎖定內部對象,沒有死鎖檢測;
- lock:用來鎖定數據庫中的對象,比如表、頁、行,有死鎖檢測機制;
可以使用show engine innodb
mutex
來查看latch的情況(一般不怎麼關心),下面我們來重點看lock。
鎖機制
基礎概念
鎖的目的是將一個資源佔住,不讓其他人操作。更準確地可以描述爲:
禁止其他人對某個資源執行某個操作。
數據庫操作無非是讀、寫,那麼相應的鎖類型爲:
- 共享鎖(S):允許事務讀數據;
- 排他鎖(X):允許事務修改數據;
鎖並非都互斥(有你沒他),比如兩個事務可以對同一條記錄加S鎖來讀數據。共存即兼容:
兼容性 | X | S |
---|---|---|
X | 不兼容 | 不兼容 |
S | 不兼容 | 兼容 |
此外還支持一種額外的鎖方式(意向鎖):
- 意向共享鎖(IS)
- 意向排他鎖(IX)
其含義是:
在行上加普通鎖之前,需要在更粗的粒度上加對應類型的意向鎖,也就是意向鎖反映了行鎖的情況,能提升加鎖效率。
比如,事務A想對錶加S鎖,需要判斷表中是否有行有X鎖。以前需要逐行判斷,而現在直接判斷表上有沒有IX鎖即可。數據庫中層次結構如下:
包含意向鎖的兼容性爲:
兼容性 | IS | IX | S | X |
---|---|---|---|---|
IS | 兼容 | 兼容 | 兼容 | 不兼容 |
IX | 兼容 | 兼容 | 不兼容 | 不兼容 |
S | 兼容 | 不兼容 | 兼容 | 不兼容 |
X | 不兼容 | 不兼容 | 不兼容 | 不兼容 |
加鎖算法
不同算法的區別在於範圍:
- Record Lock:鎖定一行記錄;
- Gap Lock:鎖定一個範圍,不包含記錄;
- Next-Key Lock:鎖定一個範圍,包含記錄;
死鎖
在兩個及以上的事務在執行過程中,可能因爲爭奪資源而互相等待:
- T1擁有資源A,嘗試獲取資源B;
- T2擁有資源B,嘗試獲取資源A;
如果不暴力終結某個事務,T1、T2會永遠等下去,這就是死鎖。數據庫通過wait-for graph(等待圖)來檢測是否存在死鎖,當發現有迴路時,引擎會選擇回滾undo量最小的事務。
程序上爲防止死鎖一般會統一更新順序。
加鎖分析
下面對常見的幾條SQL做加鎖分析,建表如下:
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
來看整體情況,需要增加鎖監控:
create table innodb_lock_monitor(x int) engine=innodb;
纔可以看到比較詳細的鎖信息。
RC+主鍵
select * from my_test where id = 1 for update;
加鎖信息:
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+唯一約束
select * from my_test where a = 1 and b = 1 for update;
加鎖信息如下:
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+普通索引
select * from my_test where c = 3 for update;
加鎖信息如下:
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+無索引
select * from my_test where d = 1 for update;
加鎖信息:
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中執行:
delete from my_test where c = 3;
在事務2中執行:
insert into my_test values(6, 6, 6, 3, 6);
查看事務2的鎖信息如下:
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:
begin;
select * from my_test where id = 1 for update;
事務2:
begin;
select * from my_test where id = 2 for update;
事務1:
select * from my_test where id = 2 for update;
此時處於鎖等待,接着事務2:
select * from my_test where id = 1 for update;
此時事務2會報死鎖,然後將事務回滾,查看死鎖信息如下:
------------------------
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:
begin;
insert into my_test values (7, 7, 7, 0, 0);
開啓事務2:
begin;
insert into my_test values (8, 7, 7, 0, 0);
此時會鎖等待,再開啓事務3:
begin;
insert into my_test values (9, 7, 7, 0, 0);
此時的鎖情況爲:
原因:
事務1在插入是需要加X鎖,而事務2、事務3在插入前需要做唯一性檢查,所以需要加S鎖,因爲與X鎖不兼容所以事務2、3處於等待狀態。
接着事務1進行rollback話,事務2會插入成功,事務3會報死鎖:
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瞭解的還是很少,最近把鎖相關的突擊了一下,抽時間把底層的實現也瞭解一下,其他部分再慢慢補上。