InnoDB 的鎖機制

InnoDB 的鎖機制

閱讀 832
收藏 57
2016-11-25
原文鏈接:owl-pi.com

寫在前面

使用數據庫時,想要較高的吞吐、較低的延遲,但又想在高併發下可以一致地讀寫數據,因此需要高效的鎖機制。

InnoDB中的鎖可以分爲:

  • latch:程序上的鎖機制,用來鎖定內部對象,沒有死鎖檢測;
  • lock:用來鎖定數據庫中的對象,比如表、頁、行,有死鎖檢測機制;

可以使用show engine innodb mutex來查看latch的情況(一般不怎麼關心),下面我們來重點看lock。

鎖機制

基礎概念

鎖的目的是將一個資源佔住,不讓其他人操作。更準確地可以描述爲:

禁止其他人對某個資源執行某個操作。

數據庫操作無非是讀、寫,那麼相應的鎖類型爲:

  • 共享鎖(S):允許事務讀數據;
  • 排他鎖(X):允許事務修改數據;

鎖並非都互斥(有你沒他),比如兩個事務可以對同一條記錄加S鎖來讀數據。共存即兼容:

兼容性 X S
X 不兼容 不兼容
S 不兼容 兼容

此外還支持一種額外的鎖方式(意向鎖):

  • 意向共享鎖(IS)
  • 意向排他鎖(IX)

其含義是:

在行上加普通鎖之前,需要在更粗的粒度上加對應類型的意向鎖,也就是意向鎖反映了行鎖的情況,能提升加鎖效率。

比如,事務A想對錶加S鎖,需要判斷表中是否有行有X鎖。以前需要逐行判斷,而現在直接判斷表上有沒有IX鎖即可。數據庫中層次結構如下:

image

包含意向鎖的兼容性爲:

兼容性 IS IX S X
IS 兼容 兼容 兼容 不兼容
IX 兼容 兼容 不兼容 不兼容
S 兼容 不兼容 兼容 不兼容
X 不兼容 不兼容 不兼容 不兼容

加鎖算法

不同算法的區別在於範圍:

  1. Record Lock:鎖定一行記錄;
  2. Gap Lock:鎖定一個範圍,不包含記錄;
  3. 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`)
  );

初始化數據如下:

image

在執行完成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

加鎖邏輯爲:

  1. 表上加IX鎖;
  2. 行上加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         ;;

加鎖邏輯爲:

  1. 表上加IX鎖;
  2. 唯一約束上加X鎖;
  3. 行上加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         ;;

加鎖邏輯爲:

  1. 表上加IX鎖;
  2. 索引上加X鎖(2);
  3. 行上加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         ;;

加鎖邏輯:

  1. 表上加X鎖;
  2. 行上加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如下:

image

而對於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);

此時的鎖情況爲:

image

原因:

事務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瞭解的還是很少,最近把鎖相關的突擊了一下,抽時間把底層的實現也瞭解一下,其他部分再慢慢補上。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章