MySQL的死鎖系列- 鎖的類型以及加鎖原理

疫情期間在家工做時,同事使用了 insert into on duplicate key update 語句進行插入去重,可是在測試過程當中發現了死鎖現象:html

ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

因爲開發任務緊急,只是暫時規避了一下,可是對觸發死鎖的緣由和相關原理不甚瞭解,因而這幾天一直在查閱相關資料,總結出一個系列文章供你們參考。本篇是上篇,主要介紹 MySQL 加鎖原理和鎖的不一樣模式或類型的基本知識。後續會講解常見語句的加鎖狀況和經過 MySQL 死鎖日誌分析死鎖緣由。mysql

因爲本篇文章涉及不少 MySQL 的基礎知識,你們能夠自行閱讀我以前的 MySQL系列文章 《MySQL探祕》中的對應章節。sql

表鎖和行鎖

咱們首先來了解一下表鎖和行鎖:表鎖是指對一整張表加鎖,通常是 DDL 處理時使用;而行鎖則是鎖定某一行或者某幾行,或者行與行之間的間隙。數據庫

表鎖由 MySQL Server 實現,行鎖則是存儲引擎實現,不一樣的引擎實現的不一樣。在 MySQL 的經常使用引擎中 InnoDB 支持行鎖,而 MyISAM 則只能使用 MySQL Server 提供的表鎖。
併發

表鎖

表鎖由 MySQL Server 實現,通常在執行 DDL 語句時會對整個表進行加鎖,好比說 ALTER TABLE 等操做。在執行 SQL 語句時,也能夠明確指定對某個表進行加鎖。性能

mysql> lock table user read(write); # 分爲讀鎖和寫鎖
Query OK, 0 rows affected (0.00 sec)

mysql> select * from user where id = 100; # 成功
mysql> select * from role where id = 100; # 失敗,未提早獲取該 role的讀表鎖
mysql> update user  set name = 'Tom' where id = 100; # 失敗,未提早得到user的寫表鎖

mysql> unlock tables; # 顯示釋放表鎖
Query OK, 0 rows affected (0.00 sec)

表鎖使用的是一次性鎖技術,也就是說,在會話開始的地方使用 lock 命令將後續須要用到的表都加上鎖,在表釋放前,只能訪問這些加鎖的表,不能訪問其餘表,直到最後經過 unlock tables 釋放全部表鎖。測試

除了使用 unlock tables 顯示釋放鎖以外,會話持有其餘表鎖時執行lock table 語句會釋放會話以前持有的鎖;會話持有其餘表鎖時執行 start transaction 或者 begin 開啓事務時,也會釋放以前持有的鎖。優化

行鎖

不一樣存儲引擎的行鎖實現不一樣,後續沒有特別說明,則行鎖特指 InnoDB 實現的行鎖。.net

在瞭解 InnoDB 的加鎖原理前,須要對其存儲結構有必定的瞭解。InnoDB 是聚簇索引,也就是 B+樹的葉節點既存儲了主鍵索引也存儲了數據行。而 InnoDB 的二級索引的葉節點存儲的則是主鍵值,因此經過二級索引查詢數據時,還須要拿對應的主鍵去聚簇索引中再次進行查詢。關於 InnoDB 和 MyISAM 的索引的詳細知識能夠閱讀《Mysql探索(一):B+Tree索引》一文。
rest

下面以兩條 SQL 的執行爲例,講解一下 InnoDB 對於單行數據的加鎖原理。

update user set age = 10 where id = 49;
update user set age = 10 where name = 'Tom';

第一條 SQL 使用主鍵索引來查詢,則只須要在 id = 49 這個主鍵索引上加上寫鎖;第二條 SQL 則使用二級索引來查詢,則首先在 name = Tom 這個索引上加寫鎖,而後因爲使用 InnoDB 二級索引還需再次根據主鍵索引查詢,因此還須要在 id = 49 這個主鍵索引上加寫鎖,如上圖所示。

也就是說使用主鍵索引須要加一把鎖,使用二級索引須要在二級索引和主鍵索引上各加一把鎖。

根據索引對單行數據進行更新的加鎖原理了解了,那若是更新操做涉及多個行呢,好比下面 SQL 的執行場景。

update user set age = 10 where id > 49;

上述 SQL 的執行過程以下圖所示。MySQL Server 會根據 WHERE 條件讀取第一條知足條件的記錄,而後 InnoDB 引擎會將第一條記錄返回並加鎖,接着 MySQL Server 發起更新改行記錄的 UPDATE 請求,更新這條記錄。一條記錄操做完成,再讀取下一條記錄,直至沒有匹配的記錄爲止。

這種場景下的鎖的釋放較爲複雜,有多種的優化方式,我對這塊暫時尚未了解,還請知道的小夥伴在下方留言解釋。

下面主要依次介紹 InnoDB 中鎖的模式和類型,鎖的類型是指鎖的粒度或者鎖具體加在什麼地方;而鎖模式描述的是鎖的兼容性,也就是加的是什麼鎖,好比寫鎖或者讀鎖。

內容基原本自於 MySQL 的技術文檔 innodb-lock 一章,感興趣的同窗能夠直接去閱讀原文,原文地址爲見文章末尾。

行鎖的模式

鎖的模式有:讀意向鎖,寫意向鎖,讀鎖,寫鎖和自增鎖(auto_inc),下面咱們依次來看。

讀寫鎖

讀鎖,又稱共享鎖(Share locks,簡稱 S 鎖),加了讀鎖的記錄,全部的事務均可以讀取,可是不能修改,而且可同時有多個事務對記錄加讀鎖。

寫鎖,又稱排他鎖(Exclusive locks,簡稱 X 鎖),或獨佔鎖,對記錄加了排他鎖以後,只有擁有該鎖的事務能夠讀取和修改,其餘事務都不能夠讀取和修改,而且同一時間只能有一個事務加寫鎖。

讀寫意向鎖

因爲表鎖和行鎖雖然鎖定範圍不一樣,可是會相互衝突。因此當你要加表鎖時,勢必要先遍歷該表的全部記錄,判斷是否加有排他鎖。這種遍歷檢查的方式顯然是一種低效的方式,MySQL 引入了意向鎖,來檢測表鎖和行鎖的衝突。

意向鎖也是表級鎖,也可分爲讀意向鎖(IS 鎖)和寫意向鎖(IX 鎖)。當事務要在記錄上加上讀鎖或寫鎖時,要首先在表上加上意向鎖。這樣判斷表中是否有記錄加鎖就很簡單了,只要看下錶上是否有意向鎖就好了。

意向鎖之間是不會產生衝突的,也不和 AUTO_INC 表鎖衝突,它只會阻塞表級讀鎖或表級寫鎖,另外,意向鎖也不會和行鎖衝突,行鎖只會和行鎖衝突。

自增鎖

AUTO_INC 鎖又叫自增鎖(通常簡寫成 AI 鎖),是一種表鎖,當表中有自增列(AUTO_INCREMENT)時出現。當插入表中有自增列時,數據庫須要自動生成自增值,它會先爲該表加 AUTO_INC 表鎖,阻塞其餘事務的插入操做,這樣保證生成的自增值確定是惟一的。AUTO_INC 鎖具備以下特色:

  • AUTO_INC 鎖互不兼容,也就是說同一張表同時只容許有一個自增鎖;
  • 自增值一旦分配了就會 +1,若是事務回滾,自增值也不會減回去,因此自增值可能會出現中斷的狀況。

顯然,AUTO_INC 表鎖會致使併發插入的效率下降,爲了提升插入的併發性,MySQL 從 5.1.22 版本開始,引入了一種可選的輕量級鎖(mutex)機制來代替 AUTO_INC 鎖,能夠經過參數 innodb_autoinc_lock_mode 來靈活控制分配自增值時的併發策略。具體能夠參考 MySQL 的 AUTO_INCREMENT Handling in InnoDB 一文,連接在文末。

不一樣模式鎖的兼容矩陣

下面是各個表鎖之間的兼容矩陣。

總結起來有下面幾點:

  • 意向鎖之間互不衝突;
  • S 鎖只和 S/IS 鎖兼容,和其餘鎖都衝突;
  • X 鎖和其餘全部鎖都衝突;
  • AI 鎖只和意向鎖兼容;

行鎖的類型

根據鎖的粒度能夠把鎖細分爲表鎖和行鎖,行鎖根據場景的不一樣又能夠進一步細分,依次爲 Next-Key Lock,Gap Lock 間隙鎖,Record Lock 記錄鎖和插入意向 GAP 鎖。

不一樣的鎖鎖定的位置是不一樣的,好比說記錄鎖只鎖住對應的記錄,而間隙鎖鎖住記錄和記錄之間的間隔,Next-Key Lock 則所屬記錄和記錄以前的間隙。不一樣類型鎖的鎖定範圍大體以下圖所示。

下面咱們來依次瞭解一下不一樣的類型的鎖。

記錄鎖

記錄鎖是最簡單的行鎖,並無什麼好說的。上邊描述 InnoDB 加鎖原理中的鎖就是記錄鎖,只鎖住 id = 49 或者 name = 'Tom' 這一條記錄。

當 SQL 語句沒法使用索引時,會進行全表掃描,這個時候 MySQL 會給整張表的全部數據行加記錄鎖,再由 MySQL Server 層進行過濾。可是,在 MySQL Server 層進行過濾的時候,若是發現不知足 WHERE 條件,會釋放對應記錄的鎖。這樣作,保證了最後只會持有知足條件記錄上的鎖,可是每條記錄的加鎖操做仍是不能省略的。

因此更新操做必需要根據索引進行操做,沒有索引時,不只會消耗大量的鎖資源,增長數據庫的開銷,還會極大的下降了數據庫的併發性能。

間隙鎖

仍是最開始更新用戶年齡的例子,若是 id = 49 這條記錄不存在,這個 SQL 語句還會加鎖嗎?答案是可能有,這取決於數據庫的隔離級別。這種狀況下,在 RC 隔離級別不會加任何鎖,在 RR 隔離級別會在 id = 49 先後兩個索引之間加上間隙鎖。

間隙鎖是一種加在兩個索引之間的鎖,或者加在第一個索引以前,或最後一個索引以後的間隙。這個間隙能夠跨一個索引記錄,多個索引記錄,甚至是空的。使用間隙鎖能夠防止其餘事務在這個範圍內插入或修改記錄,保證兩次讀取這個範圍內的記錄不會變,從而不會出現幻讀現象。

值得注意的是,間隙鎖和間隙鎖之間是互不衝突的,間隙鎖惟一的做用就是爲了防止其餘事務的插入,因此加間隙 S 鎖和加間隙 X 鎖沒有任何區別。

Next-Key 鎖

Next-key鎖是記錄鎖和間隙鎖的組合,它指的是加在某條記錄以及這條記錄前面間隙上的鎖。假設一個索引包含
1五、1八、20 ,30,49,50 這幾個值,可能的 Next-key 鎖以下:

(-∞, 15],(15, 18],(18, 20],(20, 30],(30, 49],(49, 50],(50, +∞)

一般咱們都用這種左開右閉區間來表示 Next-key 鎖,其中,圓括號表示不包含該記錄,方括號表示包含該記錄。前面四個都是 Next-key 鎖,最後一個爲間隙鎖。
和間隙鎖同樣,在 RC 隔離級別下沒有 Next-key 鎖,只有 RR 隔離級別纔有。仍是以前的例子,若是 id 不是主鍵,而是二級索引,且不是惟一索引,那麼這個 SQL 在 RR 隔離級別下就會加以下的 Next-key 鎖 (30, 49](49, 50)

此時若是插入一條 id = 31 的記錄將會阻塞住。之因此要把 id = 49 先後的間隙都鎖住,仍然是爲了解決幻讀問題,由於 id 是非惟一索引,因此 id = 49 可能會有多條記錄,爲了防止再插入一條 id = 49 的記錄。

插入意向鎖

插入意向鎖是一種特殊的間隙鎖(簡寫成 II GAP)表示插入的意向,只有在 INSERT 的時候纔會有這個鎖。注意,這個鎖雖然也叫意向鎖,可是和上面介紹的表級意向鎖是兩個徹底不一樣的概念,不要搞混了。

插入意向鎖和插入意向鎖之間互不衝突,因此能夠在同一個間隙中有多個事務同時插入不一樣索引的記錄。譬如在上面的例子中,id = 30 和 id = 49 之間若是有兩個事務要同時分別插入 id = 32 和 id = 33 是沒問題的,雖然兩個事務都會在 id = 30 和 id = 50 之間加上插入意向鎖,可是不會衝突。

插入意向鎖只會和間隙鎖或 Next-key 鎖衝突,正如上面所說,間隙鎖惟一的做用就是防止其餘事務插入記錄形成幻讀,正是因爲在執行 INSERT 語句時須要加插入意向鎖,而插入意向鎖和間隙鎖衝突,從而阻止了插入操做的執行。

不一樣類型鎖的兼容矩陣

不一樣類型鎖的兼容下以下圖所示。

其中,第一行表示已有的鎖,第一列表示要加的鎖。插入意向鎖較爲特殊,因此咱們先對插入意向鎖作個總結,以下:

  • 插入意向鎖不影響其餘事務加其餘任何鎖。也就是說,一個事務已經獲取了插入意向鎖,對其餘事務是沒有任何影響的;
  • 插入意向鎖與間隙鎖和 Next-key 鎖衝突。也就是說,一個事務想要獲取插入意向鎖,若是有其餘事務已經加了間隙鎖或 Next-key 鎖,則會阻塞。

其餘類型的鎖的規則較爲簡單:

  • 間隙鎖不和其餘鎖(不包括插入意向鎖)衝突;
  • 記錄鎖和記錄鎖衝突,Next-key 鎖和 Next-key 鎖衝突,記錄鎖和 Next-key 鎖衝突;

後記

下一篇文章咱們來看一下具體 SQL 的加鎖分析和死鎖日誌分析,請小夥伴多多留意和關注,有問題的能夠文章下方留言。

我的博客,歡迎來玩

參考

相關文章
相關標籤/搜索