在網際網路大併發應用大行其道的今天,應用的開發總是離不開鎖,在分散式應用中,最常見的莫過於基於資料庫的行級鎖了,由於網際網路公司中比較主流的資料庫還是mysql,所以這一話題繞不開的就是mysql了,但是由於mysql中innoDb引擎特殊的機制,經常一不小心就會發生死鎖,本次我們們就來聊一聊基於mysql innodb 實現的行級鎖,以及為什麼會產生死鎖,和如何避免死鎖
首先,使用mysql實現行級鎖的兩大前提就是,innodb引擎並且開啟事務。由於MySQL/InnoDB的加鎖分析,一直是一個比較困難的話題。本次我們們暫時只討論在日常應用中 select .... from table where ..... for update 語句並且在 Repeatable Read 事務隔離級別下
在此之前先明確幾個概念:
1.Index Key:
用於確定SQL查詢在索引中的連續範圍(起始範圍+結束範圍)的查詢條件,被稱之為Index Key。
2.Index Filter:
在完成Index Key的提取之後,我們根據where條件固定了索引的查詢範圍,但是此範圍中的項,並不都是滿足查詢條件的項。根據其他條件排除此範圍中不滿足的項
3.Table Filter:
所有不屬於索引列的查詢條件,均歸為Table Filter之中。
一個sql的篩選過程就是先Index Key 到 Index Filter 再到 Table Filter。
其實死鎖最大的難點可能就是很多人不知道一條for update到底是怎麼加鎖的,而在innodb引擎中行級鎖分為以下三種鎖
1.Record Lock
單個行記錄上的鎖
2.Gap Lock
間隙鎖,鎖定一個範圍,不包括記錄本身
3.Next-Key Lock
鎖定一個範圍和記錄本身
我們分以下舉例說明:
select * from table where id = 1 for update;
id 是主鍵的時候,本條sql在Index Key階段可以確定唯一一條資料,所以會在聚簇索引上加Record Lock
id 是普通索引的時候,本條sql在Index Key階段篩選出的資料不具有唯一性,所以Innodb為了防止幻度,會加Gap Lock+Next-Key Lock(Repeatable Read 事務隔離級別下,在Table Filter階段對相應的聚簇索引上加Record Lock
id 不是索引的時候,本條sql在Table Filter階段進行全表掃描,會在所有的聚簇索引上加鎖,相當於全表鎖,這是由於MySQL的實現決定的。如果一個條件無法通過索引快速過濾,那麼innodb引擎層面就會將所有記錄對應的聚簇索引加鎖後返回,然後由MySQL Server層進行過濾,在高版本的mysql中會將不符合的記錄再解鎖
select * from table where id = 1 and time = '2019-06-18' for update;
id 是主鍵,time不是索引的時候,本條sql在Index Key階段可以確定唯一一條資料,Index Filter,Table Filter 都只有一條資料,所以會在聚簇索引上加Record Lock.
id 是普通索引,time不是索引的時候,本條sql在Index Key階段篩選出的資料不具有唯一性,所以Innodb為了防止幻度,會加id和小於1的索引之間加Next-Key Lock鎖,在大於id和下一個索引之間加和Gap Lock鎖(Repeatable Read 事務隔離級別下),Table Filter階段會掃描出id = 1 範圍下所有的聚簇索引加鎖
id 不是索引,time不是索引的時候,本條sql在Table Filter階段進行全表掃描,會在所有的聚簇索引上加鎖,相當於全表鎖
id 和time都是普通索引的時候,會再id索引和time索引上分別加Next-Key Lock和Gap Lock,在Table Filter階段對相應的聚簇索引上加Record Lock
由上兩個例子得出,我們的for update 並不時都鎖一條記錄,也並不是只有一個鎖,這兩個例子基本上已經包含了for update中常見的鎖,在此基礎上我們可以根據MySQL的加鎖規則,寫出不會發生死鎖的SQL,比如,只用聚簇索引做where條件,也可以根據MySQL的加鎖規則,定位出線上產生死鎖的原因。