Innodb 鎖子系統
背景
鎖
寫在前面
本篇摘錄自,資料庫核心月報 - 2017 / 12《MySQL · 引擎特性 · Innodb 鎖子系統淺析》
link
record_lock_type 對於 LOCK_TABLE 型別來說都是空的,對於 LOCK_REC 目前值有:
#define LOCK_WAIT 256 /* 表示正在等待鎖 */
#define LOCK_ORDINARY 0 /* 表示 next-key lock ,鎖住記錄本身和記錄之前的 gap*/
#define LOCK_GAP 512 /* 表示鎖住記錄之前 gap(不鎖記錄本身) */
#define LOCK_REC_NOT_GAP 1024 /* 表示鎖住記錄本身,不鎖記錄前面的 gap */
#define LOCK_INSERT_INTENTION 2048 /* 插入意向鎖 */
#define LOCK_CONV_BY_OTHER 4096 /* 表示鎖是由其它事務建立的(比如隱式鎖轉換) */
使用位操作來設定和判斷是否設定了對應的值。
加鎖分析
對於行資料的加鎖是由函式 lock_rec_lock 完成,簡單點來看,主要的引數是 mode(鎖型別),block(包含該行的 buffer 資料頁),heap_no(具體哪一行)。就可以確定加什麼樣的鎖,以及在哪一行加。對於 mode 的值,來源於查詢的邏輯,索引和二級索引的定義,隔離級別等等。
lock fast
lock_rec_lock 首先走 lock_rec_lock_fast 邏輯,判斷能否快速完成加鎖。如果對應 block 上面一個鎖都沒有( lock_rec_get_first_on_page(block)==NULL ),那麼就建立一個鎖( lock_rec_create ),返回加鎖成功。如果 block 上已經存在鎖,滿足下面程式碼的邏輯就返回 LOCK_REC_FAIL, 快速加鎖失敗。
if (lock_rec_get_next_on_page(lock) /* 頁上是否只有一個鎖 */
|| lock->trx != trx /* 擁有鎖的事務不是當前事務 */
|| lock->type_mode != (mode | LOCK_REC)/* 已有鎖和要加的鎖模式是否相同 */
|| lock_rec_get_n_bits(lock) <= heap_no) { /* 已有鎖的 n_bits 是否滿足 heap_no */
status = LOCK_REC_FAIL;
}else if (!impl) {
/* If the nth bit of the record lock is already set
then we do not set a new lock bit, otherwise we do
set */
if (!lock_rec_get_nth_bit(lock, heap_no)) {
lock_rec_set_nth_bit(lock, heap_no);
status = LOCK_REC_SUCCESS_CREATED;
}
如果上述條件都為 false,說明:
page 上只有一個鎖
擁有該鎖的事務是當前事務
鎖模式相同
n_bits 也足夠描述大小為 heap_no 的行
那麼只需要設定一下 bitmap 就可以了(impl 表示加隱式鎖,其實也就是不加鎖)。
注:上述函式 lock_rec_get_first_on_page(block) 是從全域性 Lock_sys->hash 中拿到第一個鎖的,也就是 Hash 桶的第一個 node。
lock slow
lock fast 邏輯失敗後就會走 lock slow 邏輯,也就是上述 lock fast 判斷的
四個條件中有一個或多個為 true的時候。
lock slow 首先判斷當前事務上是否已經加了同等級或者更強級別的鎖,函式 lock_rec_has_expl,迴圈取出對應行上的所有鎖,它們要滿足以下幾個條件,就認為行上有更強的鎖。
1.基本鎖型別更強,就比如加了 LOCK_X 就不必要加 LOCK_S 了。lock_mode 基本鎖型別之間的強弱關係使用 lock_strength_matrix 判斷(lock_mode_stronger_or_eq)
static const byte lock_strength_matrix[5][5] = {
/** IS IX S X AI */
/* IS */ { TRUE, FALSE, FALSE, FALSE, FALSE},
/* IX */ { TRUE, TRUE, FALSE, FALSE, FALSE},
/* S */ { TRUE, FALSE, TRUE, FALSE, FALSE},
/* X */ { TRUE, TRUE, TRUE, TRUE, TRUE},
/* AI */ { FALSE, FALSE, FALSE, FALSE, TRUE}
};
2.不是插入意向鎖。
3.沒有等待,LOCK_WAIT 位為0
4.LOCK_REC_NOT_GAP 位為0。(沒有這個標記預設就是 NEXT KEY LOCK,鎖住行前面的gap) 或者 要加鎖的 LOCK_REC_NOT_GAP 位為 1 或者 當前行為 PAGE_HEAD_NO_SUPREMUM, 表示上界。
5.LOCK_GAP 位為0 或者 要加鎖的 LOCK_GAP 為 1 或者 當前行為 PAGE_HEAD_NO_SUPREMUM, 表示上界。
如果沒有更強級別的鎖,就要進行鎖衝突判斷,如果有鎖衝突就需要入佇列等待,並且還要進行死鎖檢測。衝突判斷呼叫函式 lock_rec_other_has_conflicting,迴圈的拿出對應行上的每一個鎖,呼叫 lock_rec_has_to_wait 進行衝突判斷。
以下描述 “鎖” 表示迴圈拿出的每個鎖,“當前鎖” 表示要加的鎖。
如果鎖和當前鎖是相同的事務,返回 false,不需要等待。
如果鎖和當前鎖的基本鎖型別相容,返回 false,不需要等待。相容性根據鎖的相容矩陣判斷。相容矩陣:
static const byte lock_compatibility_matrix[5][5] = {
/** IS IX S X AI */
/* IS */ { TRUE, TRUE, TRUE, FALSE, TRUE},
/* IX */ { TRUE, TRUE, FALSE, FALSE, TRUE},
/* S */ { TRUE, FALSE, TRUE, FALSE, FALSE},
/* X */ { FALSE, FALSE, FALSE, FALSE, FALSE},
/* AI */ { TRUE, TRUE, FALSE, FALSE, FALSE}
};
如果上述兩條都不滿足,不是相同的事務,基本鎖型別也不相容,那麼滿足下面任意一條,同樣返回false,不需要等待,否則返回 true,需要等待。
如果當前鎖鎖住的是 supremum 或者 LOCK_GAP 為 1 並且 LOCK_INSERT_INTENTION 為 0。因為不帶 LOCK_INSERT_INTENTION 的 GAP 鎖不需要等待任何東西,不同的使用者可以在 gap 上持有衝突的鎖。
如果當前鎖 LOCK_INSERT_INTENTION 為 0 並且鎖是 LOCK_GAP 為 1。因為行鎖(LOCK_ORDINARY LOCK_REC_NOT_GAP)不需要等待一個 gap 鎖。
如果當前鎖 LOCK_GAP 為 1,鎖 LOCK_REC_NOT_GAP 為 1。同樣的,因為 gap 鎖沒有必要等待一個 LOCK_REC_NOT_GAP 鎖。
如果鎖 LOCK_INSERT_INTENTION 為 1。此處是最後一步,說明之前的條件都不滿足,原始碼中備註描述如下:
No lock request needs to wait for an insertintention
lock to be removed. This is ok since our rules allow conflicting
locks on gaps. This eliminates a spurious deadlock
caused by a next-key lock waiting for an insert
intention lock; when the insert intention lock was
granted, the insert deadlocked on the waiting next-key
lock. Also, insert intention locks
do not disturb eachother.
舉個簡單的例子,如果一行資料上已經加了 LOCK_S | LOCK_REC_NOT_GAP, 再嘗試去加 LOCK_X | LOCK_GAP,LOCK_S 和 LOCK_X 本身是衝突的,但是滿足上述第 3 個條件,返回 FALSE,不需要等待。
如果行資料上沒有更強級別的鎖,也沒有衝突的鎖,並且加的不是隱式鎖,就呼叫 lock_rec_add_to_queue。核心思想是複用鎖物件,如果要加鎖的行資料上當前沒有其它鎖等待,並且行所在的資料頁上有相似的鎖物件(lock_rec_find_similar_on_page)就可以直接設定對應行的 bitmap 位,表示加鎖成功。如果有其它鎖等待,就重新建立一個鎖物件。
死鎖檢測
死鎖檢測的入口函式是 lock_deadlock_check_and_resolve,演算法是深度優先搜尋,如果在搜尋過程中發現有環,就說明發生了死鎖,為了避免死鎖檢測開銷過大,如果搜尋深度超過了 200(LOCK_MAX_DEPTH_IN_DEADLOCK_CHECK)也同樣認為發生了死鎖。
MySQL 5.7 之後增加了更多物件導向的程式碼結構,但是實際演算法並沒有改變。
稍早版本的時候,Innodb 使用的是遞迴方式搜尋,為了減少棧空間的開銷,改為使用入棧的方式。
兩個輔助資料結構:
/** Deadlock check context. */
struct lock_deadlock_ctx_t {
const trx_t* start; /*!< Joining transaction that is
requesting a lock in an incompatible
mode */
const lock_t* wait_lock; /*!< Lock that trx wants */
ib_uint64_t mark_start; /*!< Value of lock_mark_count at
the start of the deadlock check. */
ulint depth; /*!< Stack depth */
ulint cost; /*!< Calculation steps thus far */
ibool too_deep; /*!< TRUE if search was too deep and
was aborted */
};
/** DFS visited node information used during deadlock checking. */
struct lock_stack_t {
const lock_t* lock; /*!< Current lock */
const lock_t* wait_lock; /*!< Waiting for lock */
ulint heap_no; /*!< heap number if rec lock */
};
lock_stack_t 就是輔助的棧結構,使用一個 lock_stack_t 型別的陣列來作為資料棧,初始化在建立 Lock_sys 的時候,大小為 LOCK_STACK_SIZE, 實際上是 srv_max_n_thread,最大的執行緒數。
lock_deadlock_ctx_t 中的 start 始終保持不變,是第一個請求鎖的事務,如果深度搜尋過程中鎖對應的事務等於 start,那麼就說明產生了環,發生死鎖。wait_lock 表示搜尋中的事務等待的鎖。
舉個簡單的例子:
有三個事務 A,B,C 已經獲得了三行資料 1,2,3 上的 X 鎖。現在事務 A 去拿資料 2 的 X 鎖,阻塞等待。同樣事務 B 也去拿資料 3 的 X 鎖,同樣阻塞等待。當事務 C 嘗試去拿 資料 1 的 X 鎖時,發生死鎖。看下此時的死鎖檢測流程:
1.ctx 中的 start 初始化為 C,wait_lock 初始化為 X1(資料1上的X鎖)
2.根據 wait_lock=X1,呼叫函式 lock_get_first_lock 拿到加在資料 1 上的第一個鎖 lock。在例子中就是事務 A 已經獲得的 X1 鎖。
3.然後判斷 lock 對應的事務(A)是否也在等待其它鎖:lock->trx->lock.que_state == TRX_QUE_LOCK_WAIT。當前事務 A 確實在等待 X2 鎖。所以為 true,把當前的 lock 入棧(lock_dead_lock_push)。
4.ctx 中的 wait_lock 更新為 lock->trx->lock.wait_lock, 也就是 X1 鎖的持有者事務 A 所等待的鎖 X2。
5.同步驟 2 ,根據 wait_lock=X2, 拿到加在資料 2 上的第一個鎖賦值給 lock,也就是事務 B 持有的 X2 鎖。完成一次迴圈。
6.再次進入迴圈,lock 對應的事務(B)同樣在等待其它鎖,所以把當前的 lock 入棧。
7.ctx 中的 wait_lock 更新為 lock->trx->lock.wait_lock, 也就是 X2 鎖持有者事務 B 所等待的鎖 X3。
8.同步驟 2,根據 wait_lock=X3, 拿到加在資料 3 上的第一個鎖賦值給 lock,也就是事務 C 持有的 X3 鎖。完成一次迴圈。
9.再次進入迴圈,此時 lock->trx = C = ctx->start。死鎖形成。
上述例子較為簡單,沒有涉及到一行資料上有多個鎖,也沒有出棧操作,一次深度遍歷就找到了死鎖,實際情況會複雜點,其它分支可以參看原始碼理解。
show variables like 'innodb_deadlock_detect';#死鎖檢查開關
victim 選擇
當發生死鎖後,會選擇一個代價較少的事務進行回滾操作,選擇函式:lock_deadlock_select_victim(ctx)。Innodb 中的 victim 選擇比較粗暴,不論死鎖鏈條有多長,只會在 ctx->start 和 ctx->wait_lock->trx 二者中選擇其一。對應上述例子,就是在事務 B 和事務 C 中選擇。
具體的權重比較函式是 trx_weight_ge, 如果一個事務修改了不支援事務的表,那麼認為它的權重較高,否則認為 undo log 數加持有的鎖數之和較大的權重較高。
死鎖資訊分析
當發生死鎖之後,會呼叫 lock_deadlock_notify 寫入死鎖資訊,SHOW ENGINE INNODB STATUS 語句可以看到最近一次發生的死鎖資訊,因為死鎖資訊是預設寫到 temp 目錄的臨時檔案中,每次發生死鎖都會覆蓋寫。如果開啟 innodb_print_all_deadlocks可以把歷史所有的死鎖資訊列印到 errlog 中。
關於列印出來的內容具體含義有文章已經講的比較清楚了:mysql lover 和 percona。其中推薦 percona 的文章,其實發生死鎖後想找出原因的話,只有死鎖資訊是不夠的,因為 1.只顯示最近兩條事務的資訊 2.只顯示事務最近執行的一條語句。如文中推薦的做法,配合 general log 和 binlog 進行排查。
link
SHOW ENGINE INNODB STATUS 語句可以看到最近一次發生的死鎖資訊
配合 general log 和 binlog 進行排查。
鎖等待以及喚醒
鎖的等待以及喚醒實際上是執行緒的等待和喚醒,呼叫函式 lock_wait_suspend_thread 掛起當前執行緒,配合 OS_EVENT 機制,實現喚醒和鎖超時等功能
Test Case 實踐
在完成一個鎖相關 patch 的時候發現 test case 中比較詭異的點,在執行應該產生死鎖的語句時,不是每次都會產生死鎖,也會發生鎖超時的情況。以死鎖檢測中描述的例子,test case 如下:
--disable_warnings
DROP TABLE IF EXISTS t1;
--enable_warnings
create table t1(a int primary key, b int) engine=innodb;
insert into t1 values(1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7),(8,8),(9,9);
# ************* 3 Transactions cause deadlock. **************** #
# Hold locks
connection con1;
begin;
select * from t1 where a=1 for update;
connection con2;
begin;
select * from t1 where a=2 for update;
connection con3;
begin;
select * from t1 where a=3 for update;
# Request locks as a circle
connection con1;
insert into t1 values(11,11);
send select * from t1 where a=2 for update;
connection con2;
insert into t1 values(12,12);
send select * from t1 where a=3 for update;
connection con3;
--error ER_LOCK_DEADLOCK
select * from t1 where a=1 for update;
其中插入語句是為了產生 undo log,控制那一個事務會被選為 victim。上述 test case 預期產生死鎖的語句有時會報鎖超時,也就是沒有正確發生死鎖。起初以為是 victim 選擇演算法的原因,後來才發現是因為 send 語句,它只保證語句發出去,並不保證執行完畢,所以在最後一條 select 語句執行的時候也許前面的語句還沒執行完,無法產生死鎖。
使用 wait condition 語句等待下就沒問題了:
let $wait_condition=
SELECT COUNT(*) = 2 FROM information_schema.innodb_trx
WHERE trx_operation_state = 'starting index read' AND
trx_state = 'LOCK WAIT';
--source include/wait_condition.inc
--error ER_LOCK_DEADLOCK
select * from t1 where a=1 for update;
總結
Innodb 的鎖系統實際上是封裝了一層邏輯,和行本身資料一點關係也沒有,瞭解之前以為會像檔案鎖一樣,鎖的粒度越小,維護起來越複雜,所以開頭提到的 Berkeley DB 才只有頁鎖,瞭解之後很迷惑為什麼不支援行鎖… 區分一下 Innodb 同步機制使用的鎖和本文介紹的鎖是不同的,可以參考這篇月報, 有一個最不可思議的死鎖問題,就是這兩種鎖之間切換導致的。鎖系統作為事務中一個重要模組,需要配合其它模組,對於事務系統可以參考本期月報這篇文章。
MySQL · 引擎特性 · InnoDB 同步機制; MySQL · 引擎特性 · InnoDB 事務系統
本文說明,主要技術內容來自網際網路技術大佬的分享,還有一些自我的加工(僅僅起到註釋說明的作用)。如有相關疑問,請留言,將確認之後,執行侵權必刪
相關文章
- MySQL 引擎特性:InnoDB IO 子系統MySql
- MySQL·引擎特性·InnoDB事務子系統介紹MySql
- innodb查詢鎖
- InnoDB鎖學習
- MySQL鎖:03.InnoDB行鎖MySql
- Mysql innodb引擎(二)鎖MySql
- mysql innodb的行鎖MySql
- InnoDB事務鎖之行鎖-聚集索引加鎖流程索引
- MySQL鎖:InnoDB行鎖需要避免的坑MySql
- MySQL 5.5 InnoDB表鎖行鎖測試MySql
- 初探pinctrl子系統和GPIO子系統
- InnoDB常用鎖總結(行鎖、間隙鎖、臨鍵鎖、表鎖)
- InnoDB 事務加鎖分析
- mysql innodb的行鎖(2)MySql
- mysql innodb的行鎖(3)MySql
- mysql innodb的行鎖(4)MySql
- InnoDB行鎖實現方式
- mysql事務和鎖InnoDBMySql
- mysql innodb間隙鎖示例MySql
- gpio子系統與pinctrl子系統通用APIAPI
- InnoDB事務鎖之行鎖相關結構
- InnoDB事務鎖之行鎖-insert加鎖-隱式鎖加鎖原理
- InnoDB事務鎖之行鎖-delete流程update階段加鎖delete
- MySQL InnoDB 中的鎖機制MySql
- MySQL:Innodb 一個死鎖案例MySql
- MySQL 5.7 查詢InnoDB鎖表MySql
- 【MySQL】InnoDB鎖機制之一MySql
- 淺談Innodb的鎖實現
- Mysql研磨之InnoDB行鎖模式MySql模式
- MySQL InnoDB如何應付死鎖MySql
- 【MySQL】InnoDB鎖機制之二MySql
- mysql innodb的行鎖(5) --next-Key 鎖MySql
- SDD子系統
- mysql死鎖deadlock相關幾個系統變數innodb_lock_wait_timeoutMySql變數AI
- InnoDB事務鎖之行鎖-insert加鎖原理圖-聚集索引索引
- Linux記憶體子系統——Locking Pages(記憶體鎖定)Linux記憶體
- MySQL 增加InnoDB系統表空間大小MySql
- InnoDB資料字典詳解-系統表