MySQL 鎖之一——行鎖

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、參考資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • MySQL 加鎖處理分析 轉(zhuǎn)載2013年12月13日 16:43:55 7598 原文地址:http://hede...
    初來的雨天閱讀 523評(píng)論 0 2
  • 背景 MySQL/InnoDB的加鎖分析,一直是一個(gè)比較困難的話題。我在工作過程中,經(jīng)常會(huì)有同事咨詢這方面的問題。...
    MakeACoder閱讀 652評(píng)論 0 3
  • 背景 1[1.1 MVCC:Snapshot Read vs Current Read 2][1.2 ...
    jerrik閱讀 504評(píng)論 0 2
  • 聲明:本文為學(xué)習(xí)總結(jié)篇,學(xué)習(xí)自登成大大的博客,這是一篇需要反復(fù)閱讀的文章,歡迎大家一起交流學(xué)習(xí)~原文出處:何登成的...
    Vechace閱讀 429評(píng)論 1 6
  • 有多長時(shí)間沒有抬頭,看藍(lán)天一碧如洗,看白云悠悠飄過? 酷熱的天,怕曬怕熱,外出蹬車時(shí),口罩、帽子、擋風(fēng)披肩,全部武...
    所謂伊人J閱讀 357評(píng)論 3 9

友情鏈接更多精彩內(nèi)容