1、概述
MySQL 鎖以粒度劃分可以分為三類:全局鎖、表級(jí)鎖和行級(jí)鎖,MySQL 中的行級(jí)鎖由存儲(chǔ)引擎實(shí)現(xiàn),并不是所有引擎都支持行鎖,我們今天討論的是 InnoDB 下的行鎖,而 MyISAM 引擎不支持行鎖,這里不做討論;
2、行鎖種類及原理
- 行鎖分為共享鎖和排它鎖,一個(gè)事務(wù)對(duì)一行記錄加了共享鎖,則其他事務(wù)可以對(duì)其再次添加共享鎖,但不能加排它鎖,若一個(gè)事務(wù)對(duì)一行記錄加了排它鎖,則其他事務(wù)不能對(duì)該行記錄再添加任何鎖,也就是讀行為可以共享,但寫只能互斥;
- InnoDB 中的行鎖都是基于索引實(shí)現(xiàn)的,因此我們討論的行鎖有個(gè)前提,就是鎖都加在索引之上的;
2.1 行級(jí)共享鎖
2.1.1 普通行共享鎖
加鎖方式:select ... lock in share mode;
使用場景:
行級(jí)共享鎖一般用于多賬表有關(guān)聯(lián)關(guān)系,更新其中一張表的時(shí)候,防止數(shù)據(jù)不一致的這種場景;
實(shí)例演示:
創(chuàng)建兩張有關(guān)聯(lián)關(guān)系的表:
CREATE TABLE `parent` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(64) NOT NULL DEFAULT '',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `child` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`parent_id` int(11) NOT NULL,
`name` varchar(64) NOT NULL DEFAULT '',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
insert into parent(id, name) values(1, "parent001");
如果此時(shí)想往 child 表中插入一條 parent_id 為 1 的數(shù)據(jù),同時(shí) parent 表里的這行數(shù)據(jù)被其他事務(wù)刪掉了,那么就會(huì)造成 child 表的新數(shù)據(jù)無效,此時(shí)一般會(huì)使用共享鎖,如下:
select * from parent where id = 1 lock in shared mode;
insert into child(id, parent_id, name) values(1, 1, "child001");
2.1.2 間隙鎖(Gap Lock)
間隙鎖解決什么問題?
間隙鎖是 RR 隔離級(jí)別下,保證事務(wù)不會(huì)出現(xiàn)幻讀的關(guān)鍵,即 Gap 鎖保證在事務(wù)多次讀期間,滿足條件的記錄不會(huì)增多,Gap 鎖鎖定的是記錄的間隙,防止事務(wù)操作期間其他事務(wù)插入滿足條件的記錄;
間隙鎖如何工作?
看一個(gè)刪除記錄的例子:問,下面的語句會(huì)加什么鎖?
delete from user where user_id = 10;
要知道上面的語句加什么鎖,還必須要有一些前提條件,否則沒有準(zhǔn)確答案,那么要有哪些前提條件呢?有以下幾點(diǎn):
- 當(dāng)前數(shù)據(jù)庫的隔離級(jí)別是什么?
- user_id 是否是主鍵?
- user_id 若不是主鍵,在該列上是否有索引?若有索引是普通二級(jí)索引還是唯一性索引?
以上幾個(gè)條件明確了之后,我們才能確定該語句會(huì)加什么鎖。接下來我們分別看一下各種情況下該語句的加鎖情況;
1)RC 隔離級(jí)別且 user_id 為主鍵
在主鍵索引上 user_id = 10 的葉子節(jié)點(diǎn)處加記錄鎖;2)RC 隔離級(jí)別且 user_id 上有唯一性索引
在唯一性索引上 user_id = 10 的葉子節(jié)點(diǎn)處加記錄鎖,并且在對(duì)應(yīng)的主鍵索引的葉子節(jié)點(diǎn)處加記錄鎖;3)RC 隔離級(jí)別且 user_id 上有普通二級(jí)索引
在二級(jí)索引滿足 user_id = 10 的葉子節(jié)點(diǎn)處都加記錄鎖,并且在對(duì)應(yīng)的主鍵索引的葉子節(jié)點(diǎn)處都加記錄鎖;4)RC 隔離級(jí)別且 user_id 上沒有索引
InnoDB 會(huì)對(duì)聚簇索引進(jìn)行全表掃描,存儲(chǔ)引擎會(huì)對(duì)所有記錄加記錄鎖返回,由 Server 層面進(jìn)行過濾,但是,為了提升效率,MySQL 對(duì)此做了優(yōu)化,對(duì)于不滿足條件的記錄,會(huì)在判斷后放鎖,最終持有的,是滿足條件的記錄上的鎖,同時(shí),該優(yōu)化也違背了 2PL 的約束;5)RR 隔離級(jí)別且 user_id 為主鍵
同 RC 隔離級(jí)別的加鎖策略;6)RR 隔離級(jí)別且 user_id 上有唯一性索引
同 RC 隔離級(jí)別的加鎖策略;7)RR 隔離級(jí)別且 user_id 上有普通二級(jí)索引
掃描二級(jí)索引,循環(huán)查找每一條 user_id = 10 的記錄,先加記錄鎖,再加 Gap 鎖,最后在對(duì)應(yīng)的主鍵索引上加記錄鎖;8)RR 隔離級(jí)別且 user_id 上沒有索引
全表掃描,在所有記錄上加鎖,并且在所有的 Gap 處加上 Gap 鎖;
我們看到,一條簡單的 delete 語句,在不同的前提條件下會(huì)有不同的鎖被加上,而 Gap 鎖只在 RR 隔離級(jí)別且對(duì)應(yīng)的查詢列上沒有索引或有普通二級(jí)索引時(shí)會(huì)起作用,主要用來防止 RR 隔離級(jí)別下的事務(wù)產(chǎn)生幻讀;
Gap 鎖是一種共享鎖,即:不同的事務(wù)可以在同一個(gè)地方重復(fù)加 Gap 鎖,但加了 Gap 鎖之后,不允許在對(duì)應(yīng)的位置進(jìn)行插入操作;
2.1.3 插入意向鎖(Insert Intention Lock)
插入意向鎖是一種特殊的 Gap 鎖,只針對(duì)插入操作,目標(biāo)是提升插入操作的并發(fā)能力;
如果事務(wù)對(duì)記錄加了插入意向鎖,那么其他事務(wù)不能進(jìn)行加 Gap 鎖之后的查詢操作,這里并不是插入意向鎖和 Gap 鎖沖突,而是插入操作與 Gap 鎖沖突;
插入意向鎖可以理解為是一個(gè)不阻攔插入操作的 Gap 鎖,這樣多個(gè)事務(wù)在索引的同一個(gè)范圍上進(jìn)行插入操作時(shí),只要插入的記錄本身不沖突,就可以并行操作,這樣就提升了插入操作的并發(fā)性;
2.2 行級(jí)排它鎖
2.2.1 記錄鎖(Record Lock)
加鎖方式:select ... for update;
使用場景:
一種典型的場景就是先查詢再修改的場景,比如:有一個(gè)賬戶表,賬戶余額在發(fā)生變動(dòng)的時(shí)候要判斷余額大小,必須有足夠的余額才能往出轉(zhuǎn)賬,這是就需要排它鎖來確保這兩個(gè)操作是原子的,以免發(fā)生數(shù)據(jù)不一致的情況;
實(shí)例演示:
創(chuàng)建一個(gè)賬戶表
CREATE TABLE `account` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(64) NOT NULL DEFAULT '',
`balance` double NOT NULL DEFAULT 0.0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
更新賬戶余額
-- 先查詢
select balance from account where id = 1 for update;
-- 省略判斷余額是否足夠的過程
-- 再更新
update account set balance = balance - 10 where id = 1;
以上操作,如果查詢語句不加鎖,若查詢后其他事務(wù)更改了余額,就可能造成數(shù)據(jù)不一致的情況,若查詢語句使用共享鎖,則在多個(gè)事務(wù)同時(shí)操作時(shí)可能造成死鎖,因此,此處應(yīng)該使用排它鎖;
2.2.2 臨鍵鎖(Next-Key Lock)
臨鍵鎖是記錄鎖和 Gap 鎖的組合,會(huì)同時(shí)在記錄及記錄的間隙上加鎖,臨鍵鎖也是為了在 RR 隔離模式下防止幻讀;
3、總結(jié)
- InnoDB 的行鎖都是在索引上實(shí)現(xiàn)的,加鎖也是加到索引之上的;
- Gap 鎖 和 臨鍵鎖都是在 RR 隔離級(jí)別下才起作用;
- 如果一個(gè)列上沒有加索引,那么在該列上的更新或刪除操作會(huì)導(dǎo)致全表掃描,并且導(dǎo)致 InnoDB 在所有記錄上加鎖,MySQL 針對(duì)這種情況做了優(yōu)化,即:semi-consistent read;
- 行鎖分為行級(jí)共享鎖和行級(jí)排它鎖,其中,行級(jí)共享鎖有:普遍行級(jí)共享鎖、Gap 鎖、插入意向鎖,行級(jí)排它鎖有:記錄鎖、臨鍵鎖,其中插入意向鎖是一種特殊的 Gap 鎖,臨鍵鎖是 Gap 鎖和記錄鎖的組合;
4、參考資料
- 排它鎖和共享鎖:https://www.cnblogs.com/liaoweipeng/p/7615959.html
- 分析刪除操作在不同前提下的加鎖情況:http://blog.sae.sina.com.cn/archives/2127
- 插入意向鎖的解釋:http://www.itdecent.cn/p/102ad89152d9
- 插入意向鎖:https://juejin.im/post/5b865859e51d4538e331ae9a