在項目中運到鎖的案例是在分佈式項目中,採用定時任務去執行數據庫的插入操作時,由於服務部署採用的是多節點部署,在執行定時任務時有可能會造成同一時間定時任務被多個節點去執行,造成數據庫產生重複數據插入的問題。一般如果沒有加鎖的話,解決這種問題一般是在SQL中使用數據庫的唯一索引加SQL使用REPLACE INTO進行數據庫插入。還有一種方式就是使用鎖,鎖分爲:數據庫鎖和分佈式鎖。今天這裏主要說一下數據庫鎖:
InnoDB有三種行鎖:
- Record Lock(記錄鎖) :鎖住某一行記錄
- Gap Lock(間隙鎖) :鎖住一段左開右開的區間
- Next-key Lock(臨鍵鎖) :鎖住一段左開右閉的區間
哪些語句上面會加行鎖?
1)對於常見的 DML 語句(如 UPDATE
、DELETE
和 INSERT
),InnoDB 會自動給相應的記錄行加寫鎖
2)默認情況下對於普通 SELECT
語句,InnoDB 不會加任何鎖,但是在 Serializable 隔離級別下會加行級讀鎖
上面兩種是隱式鎖定,InnoDB 也支持通過特定的語句進行顯式鎖定:
3)SELECT * FROM table_name WHERE ... FOR UPDATE
,加行級寫鎖
4)SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE
,加行級讀鎖
這裏加鎖規則有兩條核心:
1)查找過程中訪問到的對象纔會加鎖
這句話該怎麼理解?比如有主鍵 id 爲
1 2 3 4 5 ... 10
的 10 條記錄,我們要找到id = 7
的記錄。注意,查找並不是從第一行開始一行一行地進行遍歷,而是根據 B+ 樹的特性進行二分查找,所以一般存儲引擎只會訪問到要找的記錄行(id = 7
)的相鄰區間
2)加鎖的基本單位是 Next-key Lock
下面結合實例幫助大夥分析一條 SQL 語句上面究竟被 InnoDB 自動加上了多少個鎖
假設有這麼一張 user
表,id
爲主鍵(唯一索引),a
是普通索引(非唯一索引),b
都是普通的列,其上沒有任何索引:
id (唯一索引) | a (非唯一索引) | b |
---|---|---|
10 | 4 | Alice |
15 | 8 | Bob |
20 | 16 | Cilly |
25 | 32 | Druid |
30 | 64 | Erik |
案例 1:唯一索引等值查詢
當我們用唯一索引進行等值查詢的時候,根據查詢的記錄是否存在,加鎖的規則會有所不同:
- 當查詢的記錄是存在的,Next-key Lock 會退化成記錄鎖
- 當查詢的記錄是不存在的,Next-key Lock 會退化成間隙鎖
查詢的記錄存在
先來看個查詢的記錄存在的案例:
1 select * from user 2 where id = 25 3 for update;
結合加鎖的兩條核心:查找過程中訪問到的對象纔會加鎖 + 加鎖的基本單位是 Next-key Lock(左開右閉),我們可以分析出,這條語句的加鎖範圍是 (20, 25]
不過,由於這個唯一索引等值查詢的記錄 id = 25
是存在的,因此,Next-key Lock 會退化成記錄鎖,因此最終的加鎖範圍是 id = 25
這一行
查詢的記錄不存在
再來看查詢的記錄不存在的案例:
1 select * from user 2 where id = 22 3 for update;
結合加鎖的兩條核心:查找過程中訪問到的對象纔會加鎖 + 加鎖的基本單位是 Next-key Lock(左開右閉),我們可以分析出,這條語句的加鎖範圍是 (20, 25]
這裏爲什麼是
(20,25]
而不是(20, 22]
,因爲 id = 22 的記錄不存在呀,InnoDB 先找到 id = 20 的記錄,發現不匹配,於是繼續往下找,發現 id = 25,因此,id = 25 的這一行被掃描到了,所以整體的加鎖範圍是(20, 25]
由於這個唯一索引等值查詢的記錄 id = 22
是不存在的,因此,Next-key Lock 會退化成間隙鎖,因此最終在主鍵 id 上的加鎖範圍是 Gap Lock (20, 25)
案例 2:唯一索引範圍查詢
唯一索引範圍查詢的規則和等值查詢的規則一樣,只有一個區別,就是唯一索引的範圍查詢需要一直向右遍歷到第一個不滿足條件的記錄,下面結合案例來分析:
1 select * from user 2 where id >= 20 and id < 22 3 for update;
先來看語句查詢條件的前半部分 id >= 20
,因此,這條語句最開始要找的第一行是 id = 20
,結合加鎖的兩個核心,需要加上 Next-key Lock (15,20]
。又由於 id 是唯一索引,且 id = 20 的這行記錄是存在的,因此會退化成記錄鎖,也就是隻會對 id = 20
這一行加鎖。
再來看語句查詢條件的後半部分 id < 22
,由於是範圍查找,就會繼續往後找第一個不滿足條件的記錄,也就是會找到 id = 25
這一行停下來,然後加 Next-key Lock (20, 25]
,重點來了,但由於 id = 25
不滿足 id < 22
,因此會退化成間隙鎖,加鎖範圍變爲 (20, 25)
。
所以,上述語句在主鍵 id 上的最終的加鎖範圍是 Record Lock id = 20
以及 Gap Lock (20, 25)
案例 3:非唯一索引等值查詢
當我們用非唯一索引進行等值查詢的時候,根據查詢的記錄是否存在,加鎖的規則會有所不同:
1、當查詢的記錄是存在的,除了會加 Next-key Lock 外,還會額外加間隙鎖(規則是向下遍歷到第一個不符合條件的值才能停止),也就是會加兩把鎖
很好記憶,就是要查找記錄的左區間加 Next-key Lock,右區間加 Gap lock
2、當查詢的記錄是不存在的,Next-key Lock 會退化成間隙鎖(這個規則和唯一索引的等值查詢是一樣的)
查詢的記錄存在
先來看個查詢的記錄存在的案例:
1 select * from user 2 where a = 16 3 for update;
結合加鎖的兩條核心,這條語句首先會對普通索引 a 加上 Next-key Lock,範圍是 (8,16]
又因爲是非唯一索引等值查詢,且查詢的記錄 a= 16
是存在的,所以還會加上間隙鎖,規則是向下遍歷到第一個不符合條件的值才能停止,因此間隙鎖的範圍是 (16,32)
所以,上述語句在普通索引 a 上的最終加鎖範圍是 Next-key Lock (8,16]
以及 Gap Lock (16,32)
查詢的記錄不存在
再來看查詢的記錄不存在的案例:
1 select * from user 2 where a = 18 3 for update;
結合加鎖的兩條核心,這條語句首先會對普通索引 a 加上 Next-key Lock,範圍是 (16,32]
但是由於查詢的記錄 a = 18
是不存在的,因此 Next-key Lock 會退化爲間隙鎖,即最終在普通索引 a 上的加鎖範圍是 (16,32)
。
案例 4:非唯一索引範圍查詢
範圍查詢和等值查詢的區別在上面唯一索引章節已經介紹過了,就是範圍查詢需要一直向右遍歷到第一個不滿足條件的記錄,和唯一索引範圍查詢不同的是,非唯一索引的範圍查詢並不會退化成 Record Lock 或者 Gap Lock。
1 select * from user 2 where a >= 16 and a < 18 3 for update;
先來看語句查詢條件的前半部分 a >= 16
,因此,這條語句最開始要找的第一行是 a = 16,結合加鎖的兩個核心,需要加上 Next-key Lock (8,16]
。雖然非唯一索引 a = 16
的這行記錄是存在的,但此時並不會像唯一索引那樣退化成記錄鎖。
再來看語句查詢條件的後半部分 a < 18
,由於是範圍查找,就會繼續往後找第一個不滿足條件的記錄,也就是會找到 id = 32
這一行停下來,然後加 Next-key Lock (16, 32]
。雖然 id = 32
不滿足 id < 18
,但此時並不會向唯一索引那樣退化成間隙鎖。
所以,上述語句在普通索引 a 上的最終的加鎖範圍是 Next-key Lock (8, 16]
和 (16, 32]
,也就是 (8, 32]
。