本篇文章給大家?guī)?lái)了關(guān)于mysql中鎖的各種類(lèi)型與模式的相關(guān)知識(shí),希望對(duì)大家有幫助。
在日常開(kāi)發(fā)工作中,我們幾乎需要天天與數(shù)據(jù)庫(kù)打交道,作為一名只會(huì)CRUD的SQL BOY,除了每天用mybatis-generator自動(dòng)生成DAO層代碼之外,我們幾乎不用去care數(shù)據(jù)庫(kù)中如何處理并發(fā)請(qǐng)求,但是突然某一天MYSQL數(shù)據(jù)庫(kù)告警了,出現(xiàn)了死鎖,我們的內(nèi)心慌的一匹,不禁想問(wèn):這不就是個(gè)普通查詢嗎,咋還鎖起來(lái)了?
為了避免慌亂的表情被主管捕捉到,我們需要提前了解一下數(shù)據(jù)庫(kù)中到底有哪些鎖。
在MySQL中,其實(shí)將鎖分成了兩類(lèi):鎖類(lèi)型(lock_type)和鎖模式(lock_mode)。
鎖類(lèi)型描述的鎖的粒度,也就是把鎖具體加在什么地方;而鎖模式描述的是到底加的是什么鎖,是讀鎖還是寫(xiě)鎖。鎖模式通常和鎖類(lèi)型結(jié)合使用。
按鎖的模式分
讀鎖
讀鎖,又叫共享鎖/S鎖/share locks。
讀鎖是某個(gè)事務(wù)(比如事務(wù)A)在進(jìn)行讀取操作(比如讀一張表或者讀取某一行)時(shí)創(chuàng)建出來(lái)的鎖,其他的事務(wù)可以并發(fā)地讀取這些數(shù)據(jù)(被加了鎖的),但是不能修改這些數(shù)據(jù)(除非持有鎖的用戶已經(jīng)釋放鎖)。
事務(wù)A對(duì)數(shù)據(jù)加上讀鎖之后,其他事務(wù)依然可以對(duì)其添加讀鎖(共享),但是不能添加寫(xiě)鎖。
在記錄上加讀鎖
InnoDB支持表鎖和行鎖,在行(也就是記錄)上加鎖,并不是鎖住該條記錄,而是在記錄對(duì)應(yīng)的索引上加鎖。如果where條件中不走索引,則會(huì)對(duì)所有的記錄加鎖。
顯式加鎖語(yǔ)句為:
注意:這里所說(shuō)的讀,是指當(dāng)前讀,快照讀是無(wú)需加鎖的。普通select讀一般都是快照讀,除了select…lock in share mode這樣的顯式加鎖語(yǔ)句下會(huì)變成當(dāng)前讀,在InnoDB引擎的serializable級(jí)別下,普通select讀也會(huì)變成快照讀。
另外需要注意,對(duì)于行鎖的加鎖過(guò)程分析,要根據(jù)事務(wù)隔離級(jí)別、是否使用索引(哪種類(lèi)型的索引)、記錄是否存在等因素結(jié)合分析,才能判斷在哪里加上了鎖。
innodb引擎中的加讀鎖的幾種情形
普通查詢?cè)诟綦x級(jí)別為 serializable 會(huì)給記錄加S鎖。但這也取決于場(chǎng)景:非事務(wù)讀(auto-commit)在 Serializable 隔離級(jí)別下,無(wú)需加鎖;
Serializable隔離級(jí)別時(shí):如果查詢條件為唯一索引且是唯一等值查詢時(shí):是在該條記錄上加S鎖;非唯一條件查詢(查詢會(huì)掃描到多條記錄時(shí)):記錄本身+記錄的間隙(需要具體分析間隙的范圍),加S鎖;
select … in share mode,會(huì)給記錄加S鎖,但是根據(jù)隔離級(jí)別的不同,加鎖的行為有所不同:
RC隔離級(jí)別:是在記錄上加S鎖。RR/Serializable隔離級(jí)別:如果查詢條件為唯一索引且是唯一等值查詢時(shí):是在該條記錄上加S鎖;非唯一條件查詢(查詢會(huì)掃描到多條記錄時(shí)):記錄本身+記錄的間隙(需要具體分析間隙的范圍),加S鎖;
通常insert操作是不加鎖的,但如果在插入或更新記錄時(shí),檢查到 duplicate key(或者有一個(gè)被標(biāo)記刪除的duplicate key),對(duì)于普通的insert/update,會(huì)加S鎖,而對(duì)于類(lèi)似replace into或者insert … on duplicate 這樣的SQL語(yǔ)句加的是X鎖。
insert … select 插入數(shù)據(jù)時(shí),會(huì)對(duì) select 的表上掃描到的數(shù)據(jù)加S鎖;
外鍵檢查:當(dāng)我們刪除一條父表上的記錄時(shí),需要去檢查是否有引用約束,這時(shí)候會(huì)掃描子表上對(duì)應(yīng)的記錄,并加上S鎖。
在表上加讀鎖
表鎖由 MySQL服務(wù)器實(shí)現(xiàn),無(wú)論存儲(chǔ)引擎是什么,都可以使用表鎖。一般在執(zhí)行 DDL 語(yǔ)句時(shí),譬如 ALTER TABLE 時(shí)就會(huì)對(duì)整個(gè)表進(jìn)行加鎖。在執(zhí)行 SQL 語(yǔ)句時(shí),也可以明確對(duì)某個(gè)表加鎖。
給表顯式加鎖語(yǔ)句為:
在使用MYISAM引擎時(shí),通常我們不需要手動(dòng)加鎖,因?yàn)镸YISAM引擎會(huì)針對(duì)我們的sql語(yǔ)句自動(dòng)進(jìn)行加鎖,整個(gè)過(guò)程不需要用戶干預(yù):
-
查詢語(yǔ)句(select):會(huì)自動(dòng)給涉及的表加讀鎖;
-
更新語(yǔ)句(update、delete、insert):會(huì)自動(dòng)給涉及的表加寫(xiě)鎖。
寫(xiě)鎖
寫(xiě)鎖,排他鎖/X鎖/exclusive locks。寫(xiě)鎖的阻塞性比讀鎖要嚴(yán)格的多,一個(gè)事務(wù)對(duì)數(shù)據(jù)添加寫(xiě)鎖之后,其他的事務(wù)對(duì)該數(shù)據(jù),既不能讀取也不能更改。
與讀鎖加鎖的范圍相同,寫(xiě)鎖既可以加在記錄上,也可以加在表上。
在記錄上加寫(xiě)鎖
在記錄上加寫(xiě)鎖,引擎需要使用InnoDB。
通常普通的select語(yǔ)句是不會(huì)加鎖的(隔離級(jí)別為Serializable除外),想要在查詢時(shí)添加排他鎖需要使用以下語(yǔ)句:
查詢時(shí)加寫(xiě)鎖:
與加讀鎖相同,寫(xiě)鎖也是加在索引上的。
更新時(shí)加寫(xiě)鎖:
在表上加寫(xiě)鎖
顯式給表加寫(xiě)鎖的語(yǔ)句為:
當(dāng)引擎選擇myisam時(shí),insert/update/delete語(yǔ)句,會(huì)自動(dòng)給該表加上排他鎖。
讀寫(xiě)鎖兼容性:
-
讀鎖是共享的,它不會(huì)阻塞其他讀鎖,但會(huì)阻塞其他的寫(xiě)鎖;
-
寫(xiě)鎖是排他的,它會(huì)阻塞其他讀鎖和寫(xiě)鎖;
-
總結(jié):讀讀不互斥,讀寫(xiě)互斥,寫(xiě)寫(xiě)互斥
意向鎖
意向鎖是一種不與行級(jí)鎖沖突的表級(jí)鎖,表示表中的記錄所需要的鎖(S鎖或X鎖)的類(lèi)型(其實(shí)就是告訴你,這張表中已經(jīng)存在了行鎖(行鎖的類(lèi)型),所以叫意向鎖)。InnoDB支持多種粒度的鎖,允許行級(jí)鎖和表級(jí)鎖的共存。
意向鎖分為:
意向共享鎖(IS鎖):IS鎖表示當(dāng)前事務(wù)意圖在表中的行上設(shè)置共享鎖
下面語(yǔ)句執(zhí)行時(shí)會(huì)首先獲取IS鎖,因?yàn)檫@個(gè)操作在獲取S鎖:獲取S鎖:select … lock in share mode
意向排它鎖(IX鎖):IX鎖表示當(dāng)前事務(wù)意圖在表中的行上設(shè)置排它鎖
下面語(yǔ)句執(zhí)行時(shí)會(huì)首先獲取IX鎖,因?yàn)檫@個(gè)操作在獲取X鎖:獲取X鎖:select … for update
事務(wù)要獲取某個(gè)表上的S鎖和X鎖之前,必須先分別獲取對(duì)應(yīng)的IS鎖和IX鎖。
意向鎖有什么作用呢:
如果另一個(gè)事務(wù)試圖在該表級(jí)別的共享鎖或排它鎖,則受到由第一個(gè)事務(wù)控制的表級(jí)別意向鎖的阻塞。第二個(gè)事務(wù)在鎖定該表前不必檢查各個(gè)頁(yè)或行鎖,而只需檢查表上的意向鎖。
示例:表test_user:
事務(wù) A 獲取了某一行的排他鎖,并未提交;
事務(wù) B 想要獲取 test_user 表的表共享鎖;
因?yàn)楣蚕礞i與排他鎖互斥,所以事務(wù) B 在試圖對(duì) test_user 表加共享鎖的時(shí)候,必須保證:
-
當(dāng)前沒(méi)有其他事務(wù)持有 users 表的排他鎖(表排他鎖)。
-
當(dāng)前沒(méi)有其他事務(wù)持有 users 表中任意一行的排他鎖(行排他鎖)。
為了檢測(cè)是否滿足第二個(gè)條件,事務(wù) B 必須在確保 test_user表不存在任何排他鎖的前提下,去檢測(cè)表中的每一行是否存在排他鎖。很明顯這是一個(gè)效率很差的做法,但是有了意向鎖之后,情況就不一樣了:
因?yàn)榇藭r(shí)事務(wù)A獲取了兩把鎖:users 表上的意向排他鎖與 id 為 28 的數(shù)據(jù)行上的排他鎖。
事務(wù) B 想要獲取 test_user 表的共享鎖:
事務(wù) B 只需要檢測(cè)事務(wù) A 是否持有 test_user 表的意向排他鎖,就可以得知事務(wù) A 必然持有該表中某些數(shù)據(jù)行的排他鎖,那么事務(wù) B 對(duì) test_users 表的加鎖請(qǐng)求就會(huì)被排斥(阻塞),從而無(wú)需去檢測(cè)表中的每一行數(shù)據(jù)是否存在排他鎖。
事務(wù) C 也想獲取 users 表中某一行的排他鎖:
-
事務(wù) C 檢測(cè)到事務(wù) A 持有 test_user 表的意向排他鎖;
-
意向鎖之間并不互斥,所以事務(wù) C 獲取到了 test_user 表的意向排他鎖;
-
因?yàn)閕d 為 31 的數(shù)據(jù)行上不存在任何排他鎖,最終事務(wù) C 成功獲取到了該數(shù)據(jù)行上的排他鎖。
意向鎖與意向鎖之間是不互斥的,但是意向鎖與其他表鎖之間存在一定的兼容互斥,具體如下:
意向鎖之間的兼容互斥性:
意向鎖與普通的排他 / 共享鎖互斥性:
自增鎖
我們?cè)谠O(shè)計(jì)表結(jié)構(gòu)的時(shí)候,通常會(huì)把主鍵設(shè)置成自增長(zhǎng)(思考一下為什么?)。
在InnoDB存儲(chǔ)引擎中,針對(duì)每個(gè)自增長(zhǎng)的字段都設(shè)置了一個(gè)自增長(zhǎng)的計(jì)數(shù)器。我們可以執(zhí)行下面的語(yǔ)句來(lái)得到這個(gè)計(jì)數(shù)器的當(dāng)前值:
當(dāng)我們進(jìn)行插入操作的時(shí)候,該操作會(huì)根據(jù)這個(gè)自增長(zhǎng)的計(jì)數(shù)器的當(dāng)前值進(jìn)行+1操作,并賦予自增長(zhǎng)的列,這個(gè)操作我們稱(chēng)之為auto-inc Locking,也就是自增長(zhǎng)鎖,這種鎖其實(shí)采用的是特殊的表鎖機(jī)制,如果insert操作出現(xiàn)在一個(gè)事務(wù)中,這個(gè)鎖是在insert操作完成之后立即釋放,而不是等待事務(wù)提交。
按鎖的類(lèi)型分
全局鎖
所謂全局鎖,其實(shí)就是給整個(gè)數(shù)據(jù)庫(kù)實(shí)例加鎖。
數(shù)據(jù)庫(kù)實(shí)例與數(shù)據(jù)庫(kù)是有所區(qū)別的:
數(shù)據(jù)庫(kù),就是保存數(shù)據(jù)的倉(cāng)庫(kù),具體到mysql中,數(shù)據(jù)庫(kù)其實(shí)是一系列數(shù)據(jù)文件集合(也就是我們通常所說(shuō)的database,比如創(chuàng)建數(shù)據(jù)庫(kù)語(yǔ)句就是 create database…)。
數(shù)據(jù)庫(kù)實(shí)例,是指訪問(wèn)數(shù)據(jù)庫(kù)的應(yīng)用程序,在Mysql中,就是mysqld進(jìn)程了。
簡(jiǎn)單來(lái)理解,數(shù)據(jù)庫(kù)實(shí)例中包含了你創(chuàng)建的各種數(shù)據(jù)庫(kù)。
如果給數(shù)據(jù)庫(kù)實(shí)例加全局鎖會(huì)導(dǎo)致整個(gè)庫(kù)處于只讀狀態(tài)(這是非常危險(xiǎn)的)。
一般來(lái)說(shuō),全局鎖的典型使用場(chǎng)景是用于全庫(kù)備份,即把數(shù)據(jù)庫(kù)中所有的表都select出來(lái)。但是要注意,讓整個(gè)庫(kù)都處于只讀狀態(tài),會(huì)導(dǎo)致一些嚴(yán)重的問(wèn)題:
-
在主庫(kù)上加全局鎖,在加鎖期間,不能執(zhí)行任何更新操作,業(yè)務(wù)基本上很多功能都不可用了;
-
在從庫(kù)上加全局鎖,在加鎖期間,不能執(zhí)行主從同步,會(huì)導(dǎo)致主從同步延遲。
全局鎖的加鎖語(yǔ)句是:
解除全局鎖的方法是:
-
斷開(kāi)執(zhí)行全局鎖的session即可;
-
執(zhí)行解鎖sql語(yǔ)句:unlock tables;
如果需要個(gè)數(shù)據(jù)庫(kù)備份的話,可以使用官方自帶的邏輯備份工具mysqldump。
既然已經(jīng)有了dump工具,為什么還需要 FTWRL 呢?一致性讀是好,但前提是引擎要支持這個(gè)隔離級(jí)別。比如,MyISAM 這種不支持事務(wù)的引擎。這時(shí),我們就需要使用 FTWRL 命令了。
FTWRL 前有讀寫(xiě)的話,F(xiàn)TWRL 都會(huì)等待讀寫(xiě)執(zhí)行完畢后才執(zhí)行。
FTWRL 執(zhí)行的時(shí)候要刷臟頁(yè)的數(shù)據(jù)到磁盤(pán),因?yàn)橐3謹(jǐn)?shù)據(jù)的一致性 ,所以執(zhí)行FTWRL時(shí)候是所有事務(wù)都提交完畢的時(shí)候。
全局鎖的實(shí)現(xiàn)還是依賴于元數(shù)據(jù)鎖的。
元數(shù)據(jù)鎖
元數(shù)據(jù)鎖(MetaData Lock),也叫MDL鎖,是用來(lái)保護(hù)元數(shù)據(jù)信息,系統(tǒng)級(jí)的鎖無(wú)法主動(dòng)控制。在MySQL5.5版本,開(kāi)始引入MDL鎖,主要是為了在并發(fā)環(huán)境下對(duì)DDL、DML同時(shí)操作下保持元數(shù)據(jù)的一致性。比如下面這種情況:
隔離級(jí)別:RR
如果沒(méi)有元數(shù)據(jù)鎖的保護(hù),那么事務(wù)2可以直接執(zhí)行DDL操作,導(dǎo)致事務(wù)1出錯(cuò)。MYSQL5.5版本的時(shí)候加入 MDL 鎖,是為了保護(hù)這種情況的發(fā)生。由于事務(wù)1開(kāi)啟了查詢,那么獲得了元數(shù)據(jù)鎖,鎖的模式為MDL讀鎖,事務(wù)2要執(zhí)行DDL,則需獲得 MDL 寫(xiě)鎖,由于讀寫(xiě)鎖互斥,所以事務(wù)2需要等待事務(wù)1釋放掉讀鎖才能執(zhí)行。
-
對(duì)表中的記錄進(jìn)行增刪改查(DML操作)的時(shí)候,自動(dòng)加MDL讀鎖;
-
對(duì)表的結(jié)構(gòu)(DDL操作)進(jìn)行修改的時(shí)候,自動(dòng)加MDL寫(xiě)鎖。
MDL鎖的粒度
MDL鎖是Mysql服務(wù)器層面中實(shí)現(xiàn)的,而不是在存儲(chǔ)引擎插件中實(shí)現(xiàn)。按照鎖定的范圍,MDL鎖可以分為以下幾類(lèi):
MDL鎖的模式
頁(yè)級(jí)鎖
MySQL中鎖定粒度介于行級(jí)鎖和表級(jí)鎖中間的一種鎖。表級(jí)鎖速度快,但沖突多,行級(jí)沖突少,但速度慢。所以取了折衷的頁(yè)級(jí),一次鎖定相鄰的一組記錄。不同的存儲(chǔ)引擎支持不同的鎖機(jī)制。根據(jù)不同的存儲(chǔ)引擎,MySQL中鎖的特性可以大致歸納如下:
頁(yè)級(jí)鎖是MySQL中比較獨(dú)特的一種鎖定級(jí)別,應(yīng)用于BDB引擎,并發(fā)度一般,頁(yè)級(jí)鎖定的特點(diǎn)是鎖定顆粒度介于行級(jí)鎖定與表級(jí)鎖之間,所以獲取鎖定所需要的資源開(kāi)銷(xiāo),以及所能提供的并發(fā)處理能力也同樣是介于上面二者之間。另外,頁(yè)級(jí)鎖定和行級(jí)鎖定一樣,會(huì)發(fā)生死鎖。
鎖定粒度大小比較:表級(jí)鎖 > 頁(yè)級(jí)鎖 > 行級(jí)鎖
表級(jí)鎖
表鎖在上文我們已經(jīng)介紹過(guò),相比于行鎖的細(xì)粒度加鎖,表鎖是對(duì)整張表加鎖。由于是對(duì)整張表加鎖,就沒(méi)有行鎖的加鎖方式那么復(fù)雜,所以加鎖比行鎖快,而且不會(huì)出現(xiàn)死鎖的情況(因?yàn)槭聞?wù)是一次性獲取想要加的表表鎖),但是表鎖也存在一些問(wèn)題:鎖的范圍過(guò)大,在并發(fā)比較高的情況下,會(huì)導(dǎo)致?lián)屾i的沖突概率變高,這樣并發(fā)性能就大打折扣了。
表鎖的加鎖方式
引擎選擇MYISAM時(shí)
MYISAM引擎只支持表鎖,不支持行鎖。
手動(dòng)添加表級(jí)鎖的語(yǔ)句如下:
在使用MYISAM引擎時(shí),通常我們不需要手動(dòng)加鎖,因?yàn)镸YISAM引擎會(huì)針對(duì)我們的sql語(yǔ)句自動(dòng)進(jìn)行加鎖,整個(gè)過(guò)程不需要用戶干預(yù):
-
查詢語(yǔ)句(select):會(huì)自動(dòng)給涉及的表加讀鎖;
-
更新語(yǔ)句(update、delete、insert):會(huì)自動(dòng)給涉及的表加寫(xiě)鎖
引擎選擇InnoDB時(shí)
InnoDB引擎同時(shí)支持行級(jí)鎖和表級(jí)鎖,默認(rèn)為行級(jí)鎖。
給InnoDB引擎的表手動(dòng)加鎖,也同樣使用 lock table {tableName} read/write 語(yǔ)句進(jìn)行讀/寫(xiě)鎖的添加。
除此之外,innodb還支持一種表級(jí)鎖:意向鎖(上文已經(jīng)介紹過(guò))。
總的來(lái)說(shuō),InnoDB引擎的表級(jí)鎖包含五種鎖模式:
-
LOCK_IS:表意向讀鎖
-
LOCK_IX:表意向?qū)戞i
-
LOCK_S:表讀鎖
-
LOCK_X:表寫(xiě)鎖
-
LOCK_AUTO_INC:自增鎖
行級(jí)鎖
在編寫(xiě)業(yè)務(wù)代碼的過(guò)程中,我們接觸最多的就是行級(jí)鎖了(表級(jí)鎖由于性能問(wèn)題,一般不推薦使用)。相比于表級(jí)鎖,行級(jí)鎖具有明顯的性能優(yōu)勢(shì):
-
沖突少:多線程中訪問(wèn)不同的記錄時(shí)只存在少量鎖定沖突;
-
鎖的粒度?。嚎梢蚤L(zhǎng)時(shí)間鎖定單一的行,對(duì)其他的行沒(méi)有影響,所以并發(fā)度是最高的;
但是使用行鎖時(shí),一旦稍不注意,是非常容易出現(xiàn)死鎖的(表鎖就不存在死鎖現(xiàn)象),所以使用行鎖需要注意加鎖的順序和鎖定的范圍。
InnoDB的行鎖是通過(guò)對(duì)索引項(xiàng)加鎖實(shí)現(xiàn)的,這表示只有通過(guò)索引查詢記錄時(shí)才會(huì)使用行鎖,如果不走索引查詢數(shù)據(jù)將使用表鎖,則性能會(huì)大打折扣。
需要記?。盒墟i也叫記錄鎖,記錄鎖都是加在索引上的。
-
where條件指定的是主鍵索引:則在主鍵索引上加鎖;
-
wehre條件指定的是二級(jí)索引:記錄鎖不僅會(huì)加在這個(gè)二級(jí)索引上,還會(huì)加在這個(gè)二級(jí)索引所對(duì)應(yīng)的聚簇索引上;
-
where條件如果無(wú)法走索引:MySQL會(huì)給整張表所有數(shù)據(jù)行加記錄鎖,存儲(chǔ)引擎層將所有記錄返回由MySQL服務(wù)端進(jìn)行過(guò)濾。
記錄鎖:LOCK_REC_NOT_GAP(只鎖記錄)
記錄鎖是最簡(jiǎn)單的行鎖。比如在RR隔離級(jí)別時(shí),執(zhí)行 select * from t_user where id = 1 for update 語(yǔ)句時(shí),實(shí)際上是對(duì) id = 1 (這里id為主鍵)這條記錄上鎖(鎖加在聚簇索引上)。
記錄鎖永遠(yuǎn)都是加在索引上的,就算一個(gè)表沒(méi)有建索引,數(shù)據(jù)庫(kù)也會(huì)隱式的創(chuàng)建一個(gè)索引。如果 WHERE 條件中指定的列是個(gè)二級(jí)索引,那么記錄鎖不僅會(huì)加在這個(gè)二級(jí)索引上,還會(huì)加在這個(gè)二級(jí)索引所對(duì)應(yīng)的聚簇索引上。
注意,如果 SQL 語(yǔ)句無(wú)法使用索引時(shí)會(huì)走主索引實(shí)現(xiàn)全表掃描,這個(gè)時(shí)候 MySQL 會(huì)給整張表的所有數(shù)據(jù)行加記錄鎖。
如果一個(gè) WHERE 條件無(wú)法通過(guò)索引快速過(guò)濾,存儲(chǔ)引擎層面就會(huì)將所有記錄加鎖后返回,再由 MySQL Server 層進(jìn)行過(guò)濾。在沒(méi)有索引時(shí),不僅會(huì)消耗大量的鎖資源,增加數(shù)據(jù)庫(kù)的開(kāi)銷(xiāo),而且極大的降低了數(shù)據(jù)庫(kù)的并發(fā)性能,所以說(shuō),更新操作一定要記得走索引(因?yàn)楦虏僮鲿?huì)加X(jué)鎖)。
行級(jí)鎖的幾種類(lèi)型:
間隙鎖:LOCK_GAP(只鎖間隙)
間隙鎖是一種區(qū)間鎖。鎖加在不存在的空閑空間上,或者兩個(gè)索引記錄之間,或者第一個(gè)索引記錄,或者最后一個(gè)索引之后的空間,用來(lái)表示只鎖住一段范圍(一般在進(jìn)行范圍查詢時(shí)且隔離級(jí)別在RR或Serializable隔時(shí))。
一般在RR隔離級(jí)別下會(huì)使用到GAP鎖。使用GAP鎖,主要是為了防止幻讀產(chǎn)生,在被GAP鎖鎖住的區(qū)間,不允許插入數(shù)據(jù)或者更新數(shù)據(jù)。
間隙鎖的產(chǎn)生條件:innodb的隔離級(jí)別為 Repeatable Read 或者 Serializable。
間隙鎖的作用范圍說(shuō)明:
隔離級(jí)別:RR
以Student表作為樣例數(shù)據(jù),id為主鍵,stu_code為學(xué)生編號(hào),添加普通索引。
間隙鎖區(qū)域定義:
根據(jù)檢索條件向左尋找最靠近的值A(chǔ),作為左區(qū)間,向右尋找最靠近的值B,作為右區(qū)間,間隙鎖為(A,B)
向左找不到最近的值A(chǔ),也是就無(wú)窮小,作為左區(qū)間,向右尋找最靠近的值B,作為右區(qū)間,間隙鎖為(無(wú)窮小,B)
向左找到最近的值A(chǔ),作為左區(qū)間,向右尋找不到最近的值B,也就是無(wú)窮大,作為右區(qū)間,間隙鎖為(A,無(wú)窮大)
區(qū)間(A,B)示例:
事務(wù)1:
select * from student where stu_code = 4 for update
事務(wù)2:
insert into student vaues(2, 2, 'A'); insert into student values(4, 5, 'B');
根據(jù)事務(wù)1的sql語(yǔ)句分析,間隙鎖的范圍是:stu_code = 4記錄是存在的,所以左區(qū)間為最近的索引值為stu_code = 3,右區(qū)間為最近的索引值為stu_code =7,所以間隙范圍為:(3,7),因此事務(wù)2的兩個(gè)insert 語(yǔ)句,一個(gè)在范圍外,一個(gè)在范圍內(nèi),在范圍外的能插入,而范圍內(nèi)的則阻塞,所以(2,2, 'A')能插入成功;(4,5, 'B')插入阻塞。
區(qū)間(無(wú)窮小,B)示例:
事務(wù)1:
select * from student where stu_code = 1 for update
事務(wù)2:
insert into student vaues(2, 0, 'c'); insert into student vaues(2, 2, 'r'); insert into student vaues(5, 2, 'o');
根據(jù)事務(wù)1的sql語(yǔ)句分析,間隙鎖的范圍是:stu_code = 1 是存在的,左邊最近沒(méi)有記錄,所以是左邊的無(wú)窮小,右邊最近的索引值為 stu_code = 3,所以間隙鎖范圍為:(無(wú)窮小,3)。所以事務(wù)2的第一個(gè)和第二個(gè)insert sql語(yǔ)句執(zhí)行被阻塞,是在間隙鎖范圍內(nèi)的。第三個(gè)insert sql語(yǔ)句能執(zhí)行成功,不在間隙鎖范圍內(nèi)。
區(qū)間(A,無(wú)窮大)示例:
事務(wù)1:
select * from student where stu_code = 7 for update
事務(wù)2:
insert into student vaues(2, 2, 'm'); insert into student vaues(20, 22, 'j');
根據(jù)事務(wù)1的sql語(yǔ)句分析,間隙鎖的范圍是:stu_code = 7 是存在的,左邊最近的索引值為 stu_code = 4,而右邊是沒(méi)有索引值的,所以間隙鎖的范圍為:(4,無(wú)窮大),第一個(gè)inset語(yǔ)句能執(zhí)行成功,不在間隙范圍內(nèi);第二個(gè)insert語(yǔ)句執(zhí)行被阻塞,是在間隙鎖范圍內(nèi)的。
如果查詢語(yǔ)句在數(shù)據(jù)庫(kù)中沒(méi)有記錄,那該怎么鎖呢?
以上是查詢是有記錄的,如果查詢語(yǔ)句在數(shù)據(jù)庫(kù)中沒(méi)有記錄,那該怎么鎖呢?咱們繼續(xù)往下:
事務(wù)1:
update student set stu_name = '000' where stu_code = 10
事務(wù)2:
insert into student vaues(2, 2, 'm'); insert into student vaues(20, 22, 'j');
根據(jù)上面的執(zhí)行語(yǔ)句是找不到記錄的,向左取最近的記錄(10,7,‘小明’)作為左區(qū)間,即間隙鎖的范圍是:(7, 無(wú)窮大),第一個(gè)insert語(yǔ)句不在區(qū)間范圍內(nèi),能執(zhí)行成功;第二個(gè)insert執(zhí)行語(yǔ)句在區(qū)間內(nèi)被阻塞,執(zhí)行失敗。如果事務(wù)1的where 條件是大于10,也是向左找最近的記錄值作為左區(qū)間,所以間隙鎖的范圍也是:(7, 無(wú)窮大)
總結(jié):間隙鎖產(chǎn)生的條件
RR/Serializable隔離級(jí)別下:Select … Where…For Update 時(shí):
只使用唯一索引查詢,并且只鎖定一條記錄時(shí),InnoDB會(huì)使用行鎖。
只使用唯一索引查詢,但是檢索條件是范圍檢索,或者是唯一檢索然而檢索結(jié)果不存在(試圖鎖住不存在的數(shù)據(jù))時(shí),會(huì)產(chǎn)生 Next-Key Lock。
使用普通索引檢索時(shí),不管是何種查詢,只要加鎖,都會(huì)產(chǎn)生間隙鎖。
同時(shí)使用唯一索引和普通索引時(shí),由于數(shù)據(jù)行是優(yōu)先根據(jù)普通索引排序,再根據(jù)唯一索引排序,所以也會(huì)產(chǎn)生間隙鎖。
下一鍵鎖:LOCK_ORDINARY,也稱(chēng)Next-Key Lock
Next-Key鎖是 record lock + gap lock 的組合。和間隙鎖一樣,在 RC 隔離級(jí)別下沒(méi)有 Next-key 鎖(除非通過(guò)修改配置強(qiáng)制開(kāi)啟),只有 RR/Serializable隔離級(jí)別才有。
MySQL InnoDB工作在可重復(fù)讀隔離級(jí)別(RR)下,并且會(huì)以Next-Key Lock的方式對(duì)數(shù)據(jù)行進(jìn)行加鎖,這樣可以有效防止幻讀的發(fā)生。Next-Key Lock是行鎖和間隙鎖的組合,當(dāng)InnoDB掃描索引記錄的時(shí)候,會(huì)首先對(duì)索引記錄加上行鎖(Record Lock),再對(duì)索引記錄兩邊的間隙加上間隙鎖(Gap Lock)。加上間隙鎖之后,其他事務(wù)就不能在這個(gè)間隙修改或者插入記錄。
當(dāng)查詢的索引含有唯一屬性(唯一索引,主鍵索引)時(shí),Innodb存儲(chǔ)引擎會(huì)對(duì)next-key lock進(jìn)行優(yōu)化,將其降為record lock,即僅鎖住索引本身,而不是范圍。
插入意向鎖:LOCK_INSERT_INTENSION
插入意向鎖,插入記錄時(shí)使用,是一種特殊的間隙鎖。這個(gè)鎖表示插入的意向,只有在執(zhí)行insert語(yǔ)句的時(shí)候才會(huì)有這個(gè)鎖。
假設(shè)有索引記錄的值分別是id = 1和id = 5(1到5之間沒(méi)有記錄),單獨(dú)的事務(wù)分別嘗試插入id = 2和 id = 3,在獲得插入行的排它鎖之前,每個(gè)事務(wù)都是用插入意圖鎖來(lái)鎖定1和5之間的空間,但是不會(huì)相互阻塞。因?yàn)椴迦胍庀蜴i之間是不會(huì)沖突的。
插入意向鎖會(huì)跟間隙鎖或者Next-Key鎖沖突:間隙鎖的作用是鎖住區(qū)間防止其他事務(wù)插入數(shù)據(jù)導(dǎo)致幻讀。
在上面的場(chǎng)景中,假設(shè)提前有事務(wù)A獲取了id 在(1,5)區(qū)間的間隙鎖,那么事務(wù)B嘗試插入 id = 2時(shí),會(huì)先嘗試獲取插入意向鎖,但是由于插入意向鎖和間隙鎖沖突,導(dǎo)致插入失敗,也就避免了幻讀產(chǎn)生。
結(jié)語(yǔ)
MYSQL的鎖機(jī)制非常復(fù)雜,在實(shí)際的開(kāi)發(fā)工作中,對(duì)于隔離級(jí)別的設(shè)置都需要非常謹(jǐn)慎,比如RR級(jí)別會(huì)比RC級(jí)別多出一個(gè)間隙鎖,這就可能導(dǎo)致嚴(yán)重的性能問(wèn)題。本文從鎖的模式和鎖的范圍對(duì)MYSQL鎖的分類(lèi)進(jìn)行了簡(jiǎn)單介紹,希望我們?cè)诿嫦驍?shù)據(jù)庫(kù)開(kāi)發(fā)的過(guò)程中,能夠仔細(xì)分析研究我們的SQL語(yǔ)句是否合理(尤其需要注意是否會(huì)產(chǎn)生死鎖等問(wèn)題)!
推薦學(xué)習(xí):mysql視頻教程