關於 MySQL 中 InnoDB 行鎖的理解及案例

Last-Modified: 2019年9月29日10:08:11mysql

本文內容主要是 《MySQL實戰45講》 課程中第 20,21,30 課程的我的筆記及相關理解.sql

主要是對於加鎖規則的理解及分析.session

如下僅針對 MySQL 的 InnoDB 引擎.併發

MyISM 引擎就是表鎖

基本概念

鎖的種類

MySQL 中的鎖主要分爲:測試

  • 全局鎖優化

    flush table with read lock;
  • 表級鎖spa

    • 表鎖rest

      lock table 表名 read;
      lock table 表名 write;
    • 元數據鎖(Meta Data Lock, MDL)
  • 行鎖
還有個自增鎖, 後續補充.

意向鎖在此先不作討論.code

InnoDB 中的鎖

行鎖

行鎖也叫作記錄鎖, 這個鎖是加在具體的索引項上的.orm

行鎖分爲兩種:

  • 讀鎖: 共享鎖
  • 寫鎖: 排它鎖

行鎖衝突狀況:

  • 讀鎖與寫鎖衝突
  • 寫鎖與寫鎖衝突

須要明確:

  • 鎖的對象是索引

間隙鎖

記錄之間是存在間隙的, 這個間隙也是能夠加上鎖實體, 稱爲間隙鎖.

間隙鎖存在的目的: 解決幻讀問題.

間隙鎖衝突狀況:

  • 間隙鎖之間是不衝突的, 它們都是爲了防止插入新的記錄.
  • 間隙鎖與插入操做(插入意向鎖)產生衝突

須要明確:

  • 間隙鎖僅在 可重複讀隔離級別下才存在.
  • 間隙鎖的概念是動態的

    對間隙(a,b)加鎖後, 存在間隙鎖 (a,b).

    此時若 a 不存在(刪除), 則間隙鎖會向左延伸直到找到一條記錄.

    若b不存在了(刪除), 則間隙鎖會向右延伸直到找到一條記錄.

    假設主鍵上存在記錄 id=5 和 id=10 和 id=15 的3條記錄, 當存在某個間隙鎖 (10,15) 時, 若咱們將 id=10 這一行刪掉, 則間隙鎖 (10, 15) 會動態擴展成 (5, 15), 此時想要插入 id=7 的記錄會被阻塞住.

    此處的刪除指的是事務提交後, 不然間隙鎖依舊是 (10,15)

next-key lock

next-key lock = 行鎖 + 間隙鎖

next-key lock 的加鎖順序:

  1. 先加間隙鎖
  2. 再加行鎖
若是加完間隙鎖後, 再加行鎖時被阻塞進入鎖等待時, 間隙鎖在此期間是不會釋放的.

索引搜索

索引搜索指的是就是:

  1. 在索引樹上利用樹搜索快速定位找到第一個值
  2. 而後向左或向右遍歷

order by desc 就是用最大的值來找第一個

order by 就是用最小的值來找第一個

等值查詢

等值查詢指的是:

  • 在索引樹上利用樹搜索快速定位 xx=yy的過程

    where xx > yy 時, 也是先找到 xx = yy 這條記錄, 這一個步驟是等值查詢.但後續的向右遍歷則屬於範圍查詢.
  • 以及在找到具體記錄後, 使用 xx=yy 向右遍歷的過程.

例子

例子1

begin;
select * from c20 where id=5 for update;

在主鍵索引 id 上快速查找到 id=5 這一行是等值查詢

例子2

begin;
select * from c20 where id > 9 and id < 12 for update;

在主鍵索引 id 上找到首個大於 9 的值, 這個過程實際上是在索引樹上快速找到 id=9 這條記錄(不存在), 找到了 (5,10) 這個間隙, 這個過程是等值查詢.

而後向右遍歷, 在遍歷過程當中就不是等值查詢了, 依次掃描到 id=10 , id=15 這兩個記錄, 其中 id=15 不符合條件, 根據優化2退化爲間隙鎖, 所以最終鎖範圍是 (5,10], (10, 15)

例子3

begin;
select * from c20 where id > 9 and id < 12 order by id desc for update;

根據語義 order by id desc, 優化器必須先找到第一個 id < 12 的值, 在主鍵索引樹上快速查找 id=12 的值(不存在), 此時是向右遍歷到 id=15, 根據優化2, 僅加了間隙鎖 (10,15) , 這個過程是等值查詢.

接着向左遍歷, 遍歷過程就不是等值查詢了, 最終鎖範圍是: (0,5], (5, 10], (10, 15)

例子4

begin;
select * from t where c>=15 and c<=20 order by c desc lock in share mode;

執行過程:

  1. 在索引c上搜索 c=20 這一行, 因爲索引c是普通索引, 所以此處的查找條件是 <u>最右邊c=20</u> 的行, 所以須要繼續向右遍歷, 直到找到 c=25 這一行, 這個過程是等值查詢. 根據優化2, 鎖的範圍是 (20, 25)
  2. 接着再向左遍歷, 以後的過程就不是等值查詢了.

例子5

begin;
select * from t where c<=20 order by c desc lock in share mode;

這裏留意一下 , 加鎖範圍並非 (20, 25], (15, 20], (10,15], (5,10], (0, 5], (-∞, 5], 而是

...........

..........

.........

........

.......

......

.....

......

.......

........

.........

..........

...........

全部行鎖+間隙鎖.

具體爲何, 其實只要 explain 看一下就明白了.

例子6 - 我的不理解的地方???????????

-- T1 事務A
begin;
select * from c20 where id>=15 and id<=20 order by id desc lock in share mode;

-- T2 事務B
begin;
update c20 set d=d+1 where id=25;    -- OK
insert into c20 values(21,21,21);    -- 阻塞

-- T3 事務A 人爲製造死鎖, 方便查看鎖狀態
update c20 set d=d+1 where id=25;    -- OK
/*
此時 事務B 提示:
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
*/

我的理解:

根據order by id desc, T1 時刻事務A首先在主鍵索引上搜索 id=20 這一行, 正常來講主鍵索引上 id=20 的只有一行, 不必向右遍歷.

但實際上, (20,25) 這個間隙被鎖上了, 且沒有對 id=25 這一行加行鎖, 初步理解是根據優化2: 索引上的等值查詢在向右遍歷且最後一個值不符合條件時, next-key lock 退化爲間隙鎖.

也就是說這個地方在搜索到 id=20 這一行後仍是繼續向右遍歷了.....不理解爲何

mysql> show engine innodb status
------------------------
LATEST DETECTED DEADLOCK
------------------------
2019-09-27 10:34:29 0xe2e8
*** (1) TRANSACTION:
TRANSACTION 1645, ACTIVE 100 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1080, 4 row lock(s), undo log entries 1
MySQL thread id 82, OS thread handle 77904, query id 61115 localhost ::1 root update
insert into c20 values(21,21,21)
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 23 page no 3 n bits 80 index PRIMARY of table `test_yjx`.`c20` trx id 1645 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 7 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
 0: len 4; hex 80000019; asc     ;;
 1: len 6; hex 00000000066d; asc      m;;
 2: len 7; hex 6e0000019a0110; asc n      ;;
 3: len 4; hex 80000019; asc     ;;
 4: len 4; hex 8000001a; asc     ;;

*** (2) TRANSACTION:
TRANSACTION 1646, ACTIVE 271 sec starting index read
mysql tables in use 1, locked 1
5 lock struct(s), heap size 1080, 5 row lock(s)
MySQL thread id 81, OS thread handle 58088, query id 61120 localhost ::1 root updating
update c20 set d=d+1 where id=25
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 23 page no 3 n bits 80 index PRIMARY of table `test_yjx`.`c20` trx id 1646 lock mode S locks gap before rec
Record lock, heap no 7 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
 0: len 4; hex 80000019; asc     ;;
 1: len 6; hex 00000000066d; asc      m;;
 2: len 7; hex 6e0000019a0110; asc n      ;;
 3: len 4; hex 80000019; asc     ;;
 4: len 4; hex 8000001a; asc     ;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 23 page no 3 n bits 80 index PRIMARY of table `test_yjx`.`c20` trx id 1646 lock_mode X locks rec but not gap waiting
Record lock, heap no 7 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
 0: len 4; hex 80000019; asc     ;;
 1: len 6; hex 00000000066d; asc      m;;
 2: len 7; hex 6e0000019a0110; asc n      ;;
 3: len 4; hex 80000019; asc     ;;
 4: len 4; hex 8000001a; asc     ;;

*** WE ROLL BACK TRANSACTION (1)

上述的:

  • (1) TRANSACTION(事務1) 指的是事務B
  • (2) TRANSACTION(事務2) 指的是事務A

注意與上面的 事務A, 事務B 順序是相反了, 別看錯了.

分析:

  • (1) TRANSACTION

    • insert into c20 values(21,21,21) 最後一句執行語句
  • (1) WAITING FOR THIS LOCK TO BE GRANTED

    • index PRIMARY of table test_yjx.c20 說明在等表 c20 主鍵索引上的鎖
    • lock_mode X locks gap before rec insert intention waiting 說明在插入一條記錄, 試圖插入一個意向鎖, 與間隙鎖產生衝突了
    • 0: len 4; hex 80000019; asc ;; 衝突的間隙鎖: 16進制的 19, 即 10進制的 id=25 左邊的間隙.
  • (2) TRANSACTION 事務2信息

    • update c20 set d=d+1 where id=25 最後一句執行語句
  • (2) HOLDS THE LOCK(S) 事務2持有鎖的信息

    • index PRIMARY of table test_yjx.c20 說明持有c20表主鍵索引上的鎖
    • lock mode S locks gap before rec 說明只有間隙鎖
    • 0: len 4; hex 80000019; asc ;; 間隙鎖: id=25 左邊的間隙
  • (2) WAITING FOR THIS LOCK TO BE GRANTED: 事務2正在等待的鎖

    • index PRIMARY of table test_yjx.c20 說明在等待 c20 表主鍵索引上的鎖
    • lock_mode X locks rec but not gap waiting 須要對行加寫鎖
    • 0: len 4; hex 80000019; asc ;; 等待給 id=25 加行鎖(寫)
  • WE ROLL BACK TRANSACTION (1) 表示回滾了事務1

我的猜想實際狀況是:

  1. 首先找到 id=20 這一條記錄, 因爲bug, 引擎認爲可能存在不止一條的 id=20 的記錄(即將其認爲是普通索引), 所以向右遍歷, 找到了 id=25 這一行, 因爲此時是等值查詢, 根據優化2, 鎖退化爲間隙鎖, 即 (20,25)
  2. 以後正常向左遍歷.

沒法證明本身的猜想. 已在課程21和課程30留下如下留言, 等待解答(或者無人解答). 2019年9月27日

-- T1 事務A
begin;
select * from c20 where id>=15 and id<=20 order by id desc lock in share mode;

-- T2 事務B
begin;
update c20 set d=d+1 where id=25;    -- OK
insert into c20 values(21,21,21);    -- 阻塞

不能理解, 爲何事務A執行的語句會給 間隙(20,25) 加上鎖.
經過 show engine innodb status; 查看發現事務A確實持有上述間隙鎖.
經過 explain select * from c20 where id>=15 and id<=20 order by id desc lock in share mode; 查看 Extra 也沒有 filesort, key=PRIMARY, 所以我的認爲是按照主鍵索引向左遍歷獲得結果.

按照個人理解, 因爲 order by id desc , 所以首先是在主鍵索引上搜索 id=20, 同時因爲主鍵索引上這個值是惟一的, 所以沒必要向右遍歷. 然而事實上它確實這麼作了, 這讓我想到了 BUG1: 主鍵索引上的範圍查詢會遍歷到不知足條件的第一個.
可是這一步的搜索過程應該是等值查詢纔對, 徹底一臉懵住了...
不知道老師如今還能看到這條評論不?

加鎖規則

該部分源自《MySQL實戰45講》中的 《21-爲何我只改了一行的語句, 鎖這麼多》

如下僅針對 MySQL 的 InnoDB 引擎在 可重複讀隔離級別, 具體MySQL版本:

  • 5.x 系列 <= 5.7.24
  • 8.0 系列 <=8.0.13

如下測試若未指定, 則默認使用如下表, 相關案例爲了不污染原始數據, 所以在不影響測試結果前提下, 都放在事務中執行, 且最終不提交.

create table c20(
    id int not null primary key, 
    c int default null, 
    d int default null, 
    key `c`(`c`)
) Engine=InnoDB;

insert into c20 values(0,0,0),(5,5,5),(10,10,10),(15,15,15),(20,20,20),(25,25,25);

/*
+----+------+------+
| id | c    | d    |
+----+------+------+
|  0 |    0 |    0 |
|  5 |    5 |    5 |
| 10 |   10 |   10 |
| 15 |   15 |   15 |
| 20 |   20 |   20 |
| 25 |   25 |   25 |
+----+------+------+
*/

2個"原則", 2個"優化", 1個"BUG"

  1. 原則1: 加鎖的基本單位是next-key lock, 前開後閉區間
  2. 原則2: 訪問到的對象纔會加鎖

    select id from t where c = 15 lock in share mode;

    加讀鎖時, 覆蓋索引優化狀況下, 不會訪問主鍵索引, 所以若是要經過 lock in share mode 給行加鎖避免數據被修改, 那就須要繞過索引優化, 如 select 一個不在索引中的值.

    但若是改爲 for update , 則 mysql 認爲接下來會更新數據, 所以會將對應主鍵索引也一塊兒鎖了

  3. 優化1: 索引上的等值查詢, 對惟一索引加鎖時, next-key lock 會退化爲行鎖

    select * from t where id = 10 for update;

    引擎會在主鍵索引上查找到 id=10 這一行, 這一個操做是等值查詢.

    鎖範圍是

  4. 優化2: 索引上的等值查詢, 向右遍歷時且最後一個值不知足等值條件時, next-key Lock 會退化爲間隙鎖

    select * from t where c = 10 for update;

    因爲索引c是普通索引, 引擎在找到 c=10 這一條索引項後繼續向右遍歷到 c=15 這一條, 此時鎖範圍是 (5, 10], (10, 15)

    select * from t where c >= 10;

    因爲索引c是普通索引, 引擎在找到 c=10 這一條索引項後繼續向右遍歷到 c=15 這一條, 此時鎖範圍是 (5, 10], (10, 15)

  5. BUG 1: 惟一索引上的範圍查詢會訪問到不知足條件的第一個值

    id> 10 and id <=15, 這時候會訪問 id=15 以及下一個記錄.

讀提交與可重複讀的加鎖區別

  1. 讀提交下沒有間隙鎖
  2. 讀提交下有一個針對 update 語句的 "semi-consistent" read 優化.

    若是 update 語句碰到一個已經被鎖了的行, 會讀入最新的版本, 而後判斷是否是知足查詢條件, 若知足則進入鎖等待, 若不知足則直接跳過.

    注意這個策略對 delete 是無效的.

  3. ?????? 語句執行過程當中加上的行鎖, 會在語句執行完成後將"不知足條件的行"上的行鎖直接釋放, 無需等到事務提交.

加鎖案例

案例: 主鍵索引 - 等值查詢 - 間隙鎖

-- T1 事務A
begin;
update c20 set d=d+1 where id=7;
/*
1. 在主鍵索引上不存在id=7記錄, 根據規則1: 加鎖基本單位是 next-key lock, 所以加鎖範圍是(5,10]
2. 因爲id=7是一個等值查詢, 根據優化2, id=10不知足條件, 所以鎖退化爲間隙鎖 (5,10)
*/

-- T2 事務B
begin;
insert into c20 values(8,8,8);        -- 阻塞
update c20 set d=d+1 where id=10;    -- OK
對應課程的案例一

案例: 非惟一索引 - 等值查詢 - 間隙鎖

-- T1 事務A
begin;
update c20 set d=d+1 where c=7;
/* 分析
1. 加鎖基本單位是next-key lock, 加鎖範圍就是 (5,10]   -- 此時只是分析過程, 並不是加鎖過程
2. 根據優化2, 索引上的等值查詢(c=7)向右遍歷且最後一個值不知足條件時, next-key lock 退化爲間隙鎖, 加鎖範圍變爲 (5, 10)
3. 因爲是在索引c上查詢, 所以加鎖範圍其實是 ((5,5), (10,10)) , 格式 (c, id)
*/

-- T2 事務B
begin;
insert into c20 values(4,5,4);    -- OK
insert into c20 values(6,5,4);    -- 被間隙鎖堵住
insert into c20 values(9,10,9);    -- 被間隙鎖堵住
insert into c20 values(11,10,9);    -- OK

案例: 非惟一索引 - 等值查詢 - 覆蓋索引

關注重點: 覆蓋索引優化致使無需回表的狀況對主鍵索引影響

-- T1 事務A
begin;
select id from c20 where c = 5 lock in share mode;    
-- 索引c是普通索引, 所以會掃描到 c=10 這一行, 所以加鎖範圍是 (0,5], (5,10]
-- 同時因爲優化2: 索引上的等值查詢向右遍歷且最後一個值不知足條件時next-key lock退化爲間隙鎖, 即加鎖範圍實際是  (0,5], (5,10)
-- 注意, 該條查詢因爲只 select id, 實際只訪問了索引c, 並無訪問到主鍵索引, 根據規則2: 訪問到的對象纔會加鎖, 所以最終只對索引c 的範圍 (0,5], (5,10) 加鎖

-- T2 事務B
begin;
update c20 set d=d+1 where id=5;    -- OK, 由於覆蓋索引優化致使並無給主鍵索引上加鎖
insert into c20 values(7,7,7);
對應課程的案例二

注意, 上面是使用 lock in share mode 加讀鎖, 所以會被覆蓋索引優化.

若是使用 for update, mysql認爲你接下來要更新行, 所以也會鎖上對應的主鍵索引.

案例: 非主鍵索引 - 範圍查詢 - 對主鍵的影響

關注重點在於: 普通索引上的範圍查詢時對不符合條件的索引加鎖時, 是否會對對應的主鍵索引產生影響.

-- T1 事務A
begin;
select * from c20 where c>=10 and c<11 for update;
/*
1. 首先查找到 c=10 這一行, 鎖範圍 (5,10]
2. 接着向右遍歷, 找到 c=15 這一行, 不符合條件, 查詢結束. 根據規則2: 只有訪問到的對象纔會加鎖, 因爲不須要訪問c=15對應的主鍵索引項, 所以這裏的鎖範圍是索引c上的 (5,10], (10,15], 以及主鍵上的行鎖[10]
*/

-- T2 事務B
begin;
select * from c20 where c=15 for update;     -- 阻塞
select * from c20 where id=15 for update;    -- OK

案例: 主鍵索引 - 範圍鎖

-- T1 事務A
begin;
select * from c20 where id>=10 and id<11 for update;
/*
1. 首先在主鍵索引上查找 id=10 這一行, 根據優化1: 索引上的等值查詢在對惟一索引加鎖時, next-key lock 退化爲行鎖, 此時加鎖範圍是 [10]
2. 繼續向右遍歷到下一個 id=15 的行, 此時並不是等值查詢, 所以加鎖範圍是 [10], (10,15]
*/

-- T2 事務B
begin;
insert into c20 values(8,8,8);        -- OK
insert into c20 values(13,13,13);    -- 阻塞
update c20 set d=d+1 where id=15;    -- 阻塞
對應課程案例三

這裏要注意, 事務A首次定位查找id=10這一行的時候是等值查詢, 然後續向右掃描到id=15的時候是範圍查詢判斷.

案例: 非惟一索引 - 範圍鎖

-- T1 事務A
begin;
select * from t where c >= 10 and c < 11 for update;
/*
1. 首先在索引c上找到 c=10 這一行, 加上鎖 (5,10]
2. 向右遍歷找到 c=15 這一行, 不知足條件, 最終加鎖範圍是 索引c上的 (5,10], (10,15], 及主鍵索引 [5]
*/

-- T2 事務B
begin;
insert into c20 values(8,8,8);        -- 阻塞
update c20 set d=d+1 where c=15;    -- 阻塞
update c20 set d=d+1 where id=15;    -- 阻塞
對應課程案例四

案例: 惟一索引 - 範圍鎖 - bug

-- T1 事務A
begin;
select * from c20 where id>10 and id<=15 for update;
/*
1. 在主鍵索引上找到 id=15 這一行, 加鎖, 根據優化1, next-key lock 退化爲行鎖 [15]
2. 向右遍歷找到 id=20 這一行, 加鎖 (15,20]
3. 最終鎖範圍是 [15], (15,20]
*/

-- T2 事務B
begin;
update c20 set d=d+1 where id=20;    -- 阻塞
insert into c20 values(16,16,16);    -- 阻塞

順便提一下:

begin;
select * from c20 where id>10 and id<15 for update;
/*
1. 在主鍵索引上找到id=15這一行, 不知足條件, 根據原則1, 加鎖 (10,15]
*/

對應課程案例五

案例: 非惟一索引 - 等值

-- T1 事務A
begin;
insert into c20 values(30,10,30);
commit;
/*
在索引c上, 此時有兩行 c=10 的行
因爲二級索引上保存着主鍵的值, 所以並不會有兩行徹底一致的行, 以下:
c    0    5    10    10    15    20    25
id    0    5    10    30    15    20    25

此時兩個 (c=10, id=10) 和 (c=10, id=30) 之間也是存在間隙的
*/

-- T2 事務B
begin;
delete from c20 where c=10;
/*
1. 首先找到索引c上 (c=10, id=10) 這一行, 加鎖 (5,10]
2. 向右遍歷, 找到 (c=10, id=30) 這一行, 加鎖 ( (c=10,id=10), (c=10,id=30) ]
3. 向右遍歷, 找到 c=20 這一行, 根據優化2, 索引上的等值查詢向右遍歷且最後一個值不匹配時, next-key lock 退化爲間隙鎖, 即加鎖 (10,15)
4. 總的加鎖範圍是 (5,10], ( (c=10,id=10), (c=10,id=30) ], (10,15]
*/

-- T3 事務C
begin;
insert into c20 values(12,12,12);    -- 阻塞
update c20 set d=d+1 where c=15;    -- OK


-- T4 掃尾, 無視
delete from c20 where id=30;
對應課程案例六

delete 的加鎖邏輯跟 select ... for update 是相似的.

案例: 非惟一索引 - limit

-- T0 初始環境
insert into c20 values(30,10,30);

-- T1 事務A
begin;
delete from c20 where c=10 limit 2;
/*
1. 找到 c=10 的第一條, 加鎖 (5,10]
2. 向右遍歷, 找到 c=10,id=30 的記錄, 加鎖 ( (c=10,id=10), (c=10,id=30) ], 此時知足 limit 2
*/

-- T2, 事務B
begin;
insert into c20 values(12,12,12);    -- OK

若是不加 limit 2 則會繼續向右遍歷找到 c=15 的記錄, 新增長鎖範圍 (10,15)

對應課程案例七

指導意義:

  • 在刪除數據時儘可能加 limit, 不只能夠控制刪除的條數, 還能夠減少加鎖的範圍.

案例: 死鎖例子

-- T1 事務A
begin;
select id from c20 where c=10 lock in share mode;
/*
1. 在索引c上找到 c=10 這一行, 因爲覆蓋索引的優化, 沒有回表, 所以只會在索引c上加鎖 (5,10]
2. 向右遍歷, 找到 c=15, 不知足, 根據優化2, 加鎖範圍退化爲 (10,15)
3. 總的加鎖範圍是在索引c上的 (5,10], (10,15)
*/

-- T2 事務B
begin;
update c20 set d=d+1 where c=10;    -- 阻塞
/*
1. 找到 c=10 這一行, 試圖加上鎖 (5,10], 按照順序先加上間隙鎖(5,10), 因爲間隙鎖之間不衝突, OK. 以後再加上 [10] 的行鎖, 但被T1時刻的事務A阻塞了, 進入鎖等待
*/

-- T3 事務A
insert into t values(8,8,8);    -- OK, 但形成 事務B 回滾
/*
往 (5,10) 這個間隙插入行, 此時與 T2時刻事務B 加的間隙鎖產生衝突.
同時因爲 事務B 也在等待 T1時刻事務A 加的行鎖, 兩個事務間存在循環資源依賴, 形成死鎖.
此時事務B被回滾了, 報錯以下:
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
*/
對應課程案例八

案例: 非主鍵索引 - 逆序

-- T1 事務A
begin;
select * from c20 where c>=15 and c<=20 order by c desc lock in share mode;
/*
1. 在索引c上找到 c=20 這一行, 加鎖 (15,20]
2. 向左遍歷, 找到 c=15 這一行, 加鎖 (10,15]
3. 繼續向左遍歷, 找到 c=10 這一行, 因爲不知足優化條件, 所以直接加鎖 (5,10], 不知足查詢條件, 中止遍歷. 
4. 最終加鎖範圍是 (5,10], (10,15], (15, 20]
*/

-- T2 事務B
insert into c20 values(6,6,6);    -- 阻塞
對應課程的上期答疑

案例: 讀提交級別 - semi-consistent 優化

-- 表結構
create table t(a int not null, b int default null)Engine=Innodb;
insert into t values(1,1),(2,2),(3,3),(4,4),(5,5);

-- T1 事務A
set session transaction isolation level read committed;
begin;
update t set a=6 where b=1;
/*
b沒有索引, 所以全表掃描, 對主鍵索引上全部行加上行鎖
*/

-- T2 事務B
set session transaction isolation level read committed;
begin;
update t set a=7 where b=2;    -- OK
/*
在讀提交隔離級別下, 若是 update 語句碰到一個已經被鎖了的行, 會讀入最新的版本, 而後判斷是否是知足查詢條件, 若知足則進入鎖等待, 若不知足則直接跳過.
*/
delete from t where b=3;    -- 阻塞
/*
注意這個策略對 delete 是無效的, 所以delete語句被阻塞
*/
對應課程評論下方 @時隱時現 2019-01-30 的留言

案例: 主鍵索引 - 動態間隙鎖 - delete

-- T1 事務A
begin;
select * from c20 where id>10 and id<=15 for update;
/*
加鎖 (10,15], (15, 20]
*/

-- T2 事務B 注意此處沒加 begin, 是立刻執行並提交的單個事務.
delete from c20 where id=10;    -- OK
/*
事務A在T1時刻加的間隙鎖 (10,15) 此時動態擴展成 (5,15)
*/

-- T3 事務C
insert into c20 values(10,10,10);    -- 阻塞
/*
被新的間隙鎖堵住了
*/
對應課程評論下方 @Geek_9ca34e 2019-01-09 的留言

若是將上方的 T2時刻的事務B 和 T3時刻的事務C 合併在一個事務裏, 則不會出現這種狀況.

我的理解是, 事務未提交時, 期間刪除/修改的數據僅僅是標記刪除/修改, 此時記錄還在, 所以間隙鎖範圍不變.

只有在事務提價後纔會進行實際的刪除/修改, 所以間隙鎖才"會動態擴大範圍"

案例: 普通索引 - 動態間隙鎖 - update

-- T1 事務A
begin;
select c from c20 where c>5 lock in share mode;
/*
找到 c=5, 不知足, 向右遍歷找到 c=10, 加鎖 (5,10], 繼續遍歷, 繼續加鎖...
*/

-- T2 事務B
update c20 set c=1 where c=5;    -- OK
/*
刪除了 c=5 這一行, 致使 T1時刻事務A 加的間隙鎖 (5,10) 變爲 (1,10)
*/

-- T3 事務C
update c20 set c=5 where c=1;    -- 阻塞
/*
將 update 理解爲兩步:
1. 插入 (c=5, id=5) 這個記錄    -- 被間隙鎖阻塞
2. 刪除 (c=1, id=5) 這個記錄
*/

案例: 非主鍵索引 - IN - 等值查詢

begin;
select id from c20 where c in (5,20,10) lock in share mode;

經過 explain 分析語句:

mysql> explain select id from c20 where c in (5,20,10) lock in share mode;
+----+-------------+-------+-------+---------------+------+---------+------+------+---------------------
| id | select_type | table | type  | possible_keys | key  | key_len | ref  | rows | Extra     
+----+-------------+-------+------------+-------+---------------+------+---------+------+------+---------
|  1 | SIMPLE      | c20   | range | c             | c    | 5       | NULL |    3 | Using where; Using index |
+----+-------------+-------+------------+-------+---------------+------+---------+------+------+---------
1 row in set, 1 warning (0.00 sec)
顯示結果太長, 所以將 partitions, filtered 列刪除了

結果分析:

  • 使用了索引 c
  • rows = 3 說明這3個值都是經過 B+ 樹搜索定位的

語句分析:

  1. 在索引c上查找 c=5, 加鎖 (0,5], 向右遍歷找到 c=10, 不知足條件, 根據優化2, 加鎖 (5,10)
  2. 在索引c上查找 c=10, 相似步驟1, 加鎖 (5,10], (10, 15)
  3. 在索引c上查找 c=20, 加鎖 (15,20], (20, 25)

注意上述鎖是一個個逐步加上去的, 而非一次性所有加上去.

考慮如下語句:

begin;
select id from c20 where c in (5,20,10) order by id desc for update;

根據語義 order by id desc, 會依次查找 c=20, c=10, c=5.

因爲加鎖順序相反, 所以若是這兩個語句併發執行的時候就有可能發生死鎖.

相關命令

查看最後一個死鎖現場

show engine innodb status;

查看 LATEST DETECTED DEADLOCK 這一節, 記錄了最後一次死鎖信息.

示例

------------------------
LATEST DETECTED DEADLOCK
------------------------
2019-09-24 16:24:18 0x5484
*** (1) TRANSACTION:
TRANSACTION 1400, ACTIVE 191 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1080, 3 row lock(s)
MySQL thread id 54, OS thread handle 74124, query id 36912 localhost ::1 root updating
update c20 set d=d+1 where c=10
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 23 page no 4 n bits 80 index c of table `test_yjx`.`c20` trx id 1400 lock_mode X waiting
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 4; hex 8000000a; asc     ;;
 1: len 4; hex 8000000a; asc     ;;

*** (2) TRANSACTION:
TRANSACTION 1401, ACTIVE 196 sec inserting
mysql tables in use 1, locked 1
5 lock struct(s), heap size 1080, 3 row lock(s), undo log entries 1
MySQL thread id 53, OS thread handle 21636, query id 36916 localhost ::1 root update
insert into c20 values(8,8,8)
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 23 page no 4 n bits 80 index c of table `test_yjx`.`c20` trx id 1401 lock mode S
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 4; hex 8000000a; asc     ;;
 1: len 4; hex 8000000a; asc     ;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 23 page no 4 n bits 80 index c of table `test_yjx`.`c20` trx id 1401 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 4; hex 8000000a; asc     ;;
 1: len 4; hex 8000000a; asc     ;;

*** WE ROLL BACK TRANSACTION (1)

結果分爲3個部分:

  • (1) TRANSACTION 第一個事務的信息

    • WAITING FOR THIS LOCK TO BE GRANTED, 表示這個事務在等待的鎖資源
  • (2) TRANSACTION 第二個事務的信息

    • HOLDS THE LOCK(S) 顯示該事務持有哪些鎖
  • WE ROLL BACK TRANSACTION (1) 死鎖檢測的處理: 回滾了第一個事務

第一個事務的信息中:

  • update c20 set d=d+1 where c=10 致使死鎖時執行的最後一條 sql 語句
  • WAITING FOR THIS LOCK TO BE GRANTED

    • index c of table test_yjx.c20, 說明在等的是表 c20 的索引 c 上面的鎖
    • lock_mode X waiting 表示這個語句要本身加一個寫鎖, 當前狀態是等待中.
    • Record lock 說明這是一個記錄鎖
    • n_fields 2 表示這個記錄是兩列, 即 字段c 和 主鍵字段 id
    • 0: len 4; hex 8000000a; asc ;; 是第一個字段(即字段c), 值(忽略裏面的8)是十六進制 a, 即 10

      值 8000000a 中的 8...我也不理解爲何, 先忽略
    • 1: len 4; hex 8000000a; asc ;; 是第二個字段(即字段id), 值是 10
    • 上面兩行裏的 asc 表示, 接下來要打印出值裏面的"可打印字符", 但10不是可打印字符, 所以就顯示空格

      這裏不太理解
  • 第一個事務信息只顯示出等鎖的狀態, 在等待 (c=10, id=10) 這一行的鎖
  • 沒有顯示當前事務持有的鎖, 但能夠從第二個事務中推測出來.

第二個事務的信息中:

  • insert into c20 values(8,8,8) 致使死鎖時最後執行的語句
  • HOLDS THE LOCK(S)

    • index c of table test_yjx.c20 trx id 1401 lock mode S 表示鎖是在表 c20 的索引 c 上, 加的是讀鎖
    • hex 8000000a;表示這個事務持有 c=10 這個記錄鎖
  • WAITING FOR THIS LOCK TO BE GRANTED

    • index c of table test_yjx.c20 trx id 1401 lock_mode X locks gap before rec insert intention waiting

      • insert intention 表示試圖插入一個記錄, 這是一個插入意向鎖, 與間隙鎖產生鎖衝突
      • gap before rec 表示這是一個間隙鎖, 而不是記錄鎖.

補充:

  • lock_mode X waiting 表示 next-key lock
  • lock_mode X locks rec but not gap 表示只有行鎖
  • locks gap before rec 就是隻有間隙鎖

從上面信息能夠知道:

  • 第一個事務

    • 推測出持有間隙鎖 (?, 10)
    • 試圖更新 c=10 這一行, 但被索引c 的 行鎖 c=10 阻塞了
  • 第二個事務

    • 持有行鎖 c=10
    • 試圖插入 (8,8,8), 但被間隙鎖 (?, 10) 阻塞了
  • 檢測到死鎖時, InnoDB 認爲 第二個事務回滾成本更高, 所以回滾了第一個事務.
相關文章
相關標籤/搜索