1. 程式人生 > >【MySQL技術內幕】35-InnoDB儲存引擎中的鎖

【MySQL技術內幕】35-InnoDB儲存引擎中的鎖

1、鎖的型別

InnoDB儲存引擎實現瞭如下兩種標準的行級鎖:

  • 共享鎖( S Lock),允許事務讀一行資料。
  • 排他鎖( X LocK),允許事務刪除或更新一行資料。

如果一個事務T1已經獲得了行r的共享鎖,那麼另外的事務T2可以立即獲得行r的共享鎖,因為讀取並沒有改變行r的資料,稱這種情況為鎖相容( Lock Compatible)。 但若有其他的事務T3想獲得行r的排他鎖,則其必須等待事務T1、T2釋放行r上的共享鎖——這種情況稱為鎖不相容。下表顯示了共享鎖和排他鎖的相容性。

排他鎖和共享鎖的相容性
X S
X 不相容 不相容
S 不相容 相容

從表中可以發現X鎖與任何的鎖都不相容,而S鎖僅和S鎖相容。需要特別注意的是,S和X鎖都是行鎖,相容是指對同一記錄(row)鎖的相容性情況。 此外, InnoDB儲存引擎支援多粒度(granular)鎖定,這種鎖定允許事務在行級上的鎖和表級上的鎖同時存在。為了支援在不同粒度上進行加鎖操作, InnoDB儲存引擎支援一種額外的鎖方式,稱之為意向鎖(Intention lock)。意向鎖是將鎖定的物件分為多個層次,意向鎖意味著事務希望在更細粒度(fine granularity)上進行加鎖,如下圖所示。

若將上鎖的物件看成一棵樹,那麼對最下層的物件上鎖,也就是對最細粒度的物件進行上鎖,那麼首先需要對粗粒度的物件上鎖。例如圖6-3,如果需要對頁上的記錄r進行上X鎖,那麼分別需要對資料庫A、表、頁上意向鎖IX,最後對記錄r上ⅹ鎖。若其中任何一個部分導致等待,那麼該操作需要等待粗粒度鎖的完成。舉例來說,在對記錄r加ⅹ鎖之前,已經有事務對錶1進行了S表鎖,那麼表1上已存在S鎖,之後事務需要對記錄r在表1上加上IX,由於不相容,所以該事務需要等待表鎖操作的完成。 InnoDB儲存引擎支援意向鎖設計比較簡練,其意向鎖即為表級別的鎖。設計目的主要是為了在一個事務中揭示下一行將被請求的鎖型別。其支援兩種意向鎖:

  1. 意向共享鎖( IS Lock),事務想要獲得一張表中某幾行的共享鎖
  2. 意向排他鎖( IX Lock),事務想要獲得一張表中某幾行的排他鎖

由於 InnoDB儲存引擎支援的是行級別的鎖,因此意向鎖其實不會阻塞除全表掃以外的任何請求。故表級意向鎖與行級鎖的相容性如下表所示。

InnoDB儲存引擎中鎖的相容性
IS IX S X
IS 相容 相容 相容 不相容
IX 相容 相容 不相容 不相容
S 相容 不相容 相容 不相容
X 不相容 不相容 不相容 不相容

使用者可以通過命令 SHOW ENGINE INNODB STATUS命令來檢視當前鎖請求的資訊:

可以看到SQL語句 select* from t where a<4 lock in share mode在等待, RECORD LOCKS space id 30 page no 3 n bits 72 index' PRIMARY 'of table'test''t trx id 48B89BD lock mode X locks rec but not gap表示鎖住的資源。 locks rec but not gap代表鎖住的是一個索引,不是一個範圍。 在InnoDB1.0版本之前,使用者只能通過命令 SHOW FULL PROCESSLIST,SHOW ENGINE INNODB STATUS等來檢視當前資料庫中鎖的請求,然後再判斷事務鎖的情況。從 InnoDB1.0開始,在 INFORMATION SCHEMA架構下添加了表 INNODB_TRX、 INNODB_LOCKS、 INNODB_LOCK_WAITS。通過這三張表,使用者可以更簡單地監控當前事務並分析可能存在的鎖問題。我們將通過具體的示例來分析這三張表,在之前,首先了來看下錶中表 INNODB_TRX的定義,其由8個欄位組成。

表INNODB_TRX的結構說明
欄位名 說明
trx_id InnoDB儲存引擎內部唯一的事務ID
trx_state 當前事務的狀態
trx_started 事務的開始時間
trx_requested_lock_ id 等待事務的鎖ID。如 trx state的狀態為 LOCK WAIT,那麼該值代表當前的事務等待之前事務佔用鎖資源的ID。若 trx state不是 LOCK WAIT,則該值為NULL
trx_wait_started 事務等待開始的時間
trx_weight 事務的權重,反映了一個事務修改和鎖住的行數。在 InnoDB儲存引擎中,當發生死鎖需要回滾時, InnoDB儲存引擎會選擇該值最小的進行回滾
trx_mysql_thread_id MySQL中的執行緒ID, SHOW PROCESSLIST顯示的結果
trx_query 事務執行的SQL語句

接著來看一個具體的例子:

通過列state可以觀察到trx_id為730FEE的事務當前正在執行,而trx_id為7311F4的事務目前處於“ LOCK WAIT”狀態,且執行的SQL語句是 select * from parent lock in share mode。該表只是顯示了當前執行的 InnoDB事務,並不能直接判斷鎖的一些情況。 如果需要檢視鎖,則還需要訪問表 INNODB_LOCKS,該表的欄位組成如下表所示。

表INNODB_LOCKS的結構
欄位名 說明
lock_id 鎖的ID
lock_trx_id 事務ID
lock_mode 鎖的模式
lock_type 鎖的型別,表鎖還是行鎖
lock_table 要加鎖的表
lock_index 鎖住的索引
lock 鎖物件的 space id
lock_page 事務鎖定頁的數量。若是表鎖,則該值為NULL
lock_rec 事務鎖定行的數量,若是表鎖,則該值為NULL
lock_data 事務鎖定記錄的主鍵值,若是表鎖,則該值為NULL

接著上面的例子,繼續查看錶 INNODB_LOCKS:

這次使用者可以清晰地看到當前鎖的資訊。 trx_id為730FEE的事務向表 parent加了一個X的行鎖,ID為7311F4的事務向表parent申請了一個S的行鎖。 lock_data都是1,申請相同的資源,因此會有等待。這也可以解釋INNODB_TRX中為什麼一個事務的trx_state是“ RUNNING”,另一個是“ LOCK WAIT”了。 另外需要特別注意的是,我發現 lock_data這個值並非是“可信”的值。例如當用戶執行一個範圍査找時, lock_data可能只返回第一行的主鍵值。與此同時,如果當前資源被鎖住了,若鎖住的頁因為 InnoDB儲存引擎緩衝池的容量,導致該頁從緩衝池中被刷出,則査看 INNODB LOCKS表時,該值同樣會顯示為NUL,即 InnoDB儲存引擎不會從磁碟進行再一次的查詢。 在通過表 INNODB_LOCKS查看了每張表上鎖的情況後,使用者就可以來判斷由此引發的等待情況了。當事務較小時,使用者就可以人為地、直觀地進行判斷了。但是當事務量非常大,其中鎖和等待也時常發生,這個時候就不這麼容易判斷。但是通過表INNODB_LOCK_WAITS,可以很直觀地反映當前事務的等待。表 INNODB_LOCKWAITS由4個欄位組成,如下表所示。

表INNODB_LOCK_WAITS的結構
欄位 說明
requesting_trx_id 申請鎖資源的事務 blocking _trx_id
blocking_trx_id 阻塞的事務ID
requesting_lock_id 申請的鎖的ID
blocking_lock_id 阻塞的鎖的ID

接著上面的例子,執行如下查詢:

mysql> SELECT* FROM information_schema.INNODB_LOCK_WAITS\G;
*****************************1.row*****************************************
requesting_trx_id: 7311F4
requested_lock_id: 7311F4:96:3:2
blocking_trx_id: 730FEE
blocking_lock_id: 730FEE:96:3:2
1 row in set (0.00 sec)

通過上述的SQL語句,使用者可以清楚直觀地看到哪個事務阻塞了另一個事務。 當然,這裡只給出了事務和鎖的ID。如果需要,使用者可以根據表 INNODB_TRX、INNODB_LOCKS、INNODB_LOCK_WAITS得到更為直觀的詳細資訊。例如,使用者可以執行如下聯合查詢:

SELECT
r.trx_id waiting_trx_id,
r.trx_mysql_thread_id waiting_thread,
r.trx_query waiting_query,
b.trx_id blocking_trx_id,
b.trx_mysql_thread_id blocking thread,
b.trx_query blocking_query
FROM information_schema.innodb_lock_waits w
INNER JOIN information_schema.innodb_trx b
ON b.trx_id = w.blocking_trx_id
INNER JOIN information_schema.innodb_trx r
ON r.trx_id = w.requesting_trx_id\G;