c++客户端发送加锁_MySQL语句加锁分析详解
前言
建立一個(gè)存儲(chǔ)三國英雄的hero表:
CREATE TABLE hero (number INT,
name VARCHAR(100),
country varchar(100),
PRIMARY KEY (number),
KEY idx_name (name)
) Engine=InnoDB CHARSET=utf8;
然后向這個(gè)表里插入幾條記錄:
INSERT INTO hero VALUES(1, 'l劉備', '蜀'),
(3, 'z諸葛亮', '蜀'),
(8, 'c曹操', '魏'),
(15, 'x荀彧', '魏'),
(20, 's孫權(quán)', '吳');
然后現(xiàn)在hero表就有了兩個(gè)索引(一個(gè)二級(jí)索引,一個(gè)聚簇索引),示意圖如下:
語句加鎖分析
其實(shí)啊,“XXX語句該加什么鎖”本身就是個(gè)偽命題,一條語句需要加的鎖受到很多條件制約,比方說:
事務(wù)的隔離級(jí)別
語句執(zhí)行時(shí)使用的索引(比如聚簇索引、唯一二級(jí)索引、普通二級(jí)索引)
查詢條件(比方說=、=<、>=等等)
具體執(zhí)行的語句類型
在繼續(xù)詳細(xì)分析語句的加鎖過程前,大家一定要有一個(gè)全局概念:加鎖只是解決并發(fā)事務(wù)執(zhí)行過程中引起的臟寫、臟讀、不可重復(fù)讀、幻讀這些問題的一種解決方案(MVCC算是一種解決臟讀、不可重復(fù)讀、幻讀這些問題的一種解決方案),一定要意識(shí)到加鎖的出發(fā)點(diǎn)是為了解決這些問題,不同情景下要解決的問題不一樣,才導(dǎo)致加的鎖不一樣,千萬不要為了加鎖而加鎖,容易把自己繞進(jìn)去。當(dāng)然,有時(shí)候因?yàn)镸ySQL具體的實(shí)現(xiàn)而導(dǎo)致一些情景下的加鎖有些不太好理解,這就得我們死記硬背了~
我們這里把語句分為3種大類:普通的SELECT語句、鎖定讀的語句、INSERT語句,我們分別看一下。
普通的SELECT語句
普通的SELECT語句在:
READ UNCOMMITTED隔離級(jí)別下,不加鎖,直接讀取記錄的最新版本,可能發(fā)生臟讀、不可重復(fù)讀和幻讀問題。
READ COMMITTED隔離級(jí)別下,不加鎖,在每次執(zhí)行普通的SELECT語句時(shí)都會(huì)生成一個(gè)ReadView,這樣解決了臟讀問題,但沒有解決不可重復(fù)讀和幻讀問題。
REPEATABLE READ隔離級(jí)別下,不加鎖,只在第一次執(zhí)行普通的SELECT語句時(shí)生成一個(gè)ReadView,這樣把臟讀、不可重復(fù)讀和幻讀問題都解決了。
不過這里有一個(gè)小插曲:
# 事務(wù)T1,REPEATABLE READ隔離級(jí)別下
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT * FROM hero WHERE number = 30;
Empty set (0.01 sec)# 此時(shí)事務(wù)T2執(zhí)行了:INSERT INTO hero VALUES(30, 'g關(guān)羽', '魏'); 并提交
mysql> UPDATE hero SET country = '蜀' WHERE number = 30;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> SELECT * FROM hero WHERE number = 30;
+--------+---------+---------+
| number | name | country |
+--------+---------+---------+
| 30 | g關(guān)羽 | 蜀 |
+--------+---------+---------+
1 row in set (0.01 sec)在REPEATABLE READ隔離級(jí)別下,T1第一次執(zhí)行普通的SELECT語句時(shí)生成了一個(gè)ReadView,之后T2向hero表中新插入了一條記錄便提交了,ReadView并不能阻止T1執(zhí)行UPDATE或者DELETE語句來對(duì)改動(dòng)這個(gè)新插入的記錄(因?yàn)門2已經(jīng)提交,改動(dòng)該記錄并不會(huì)造成阻塞),但是這樣一來這條新記錄的trx_id隱藏列就變成了T1的事務(wù)id,之后T1中再使用普通的SELECT語句去查詢這條記錄時(shí)就可以看到這條記錄了,也就把這條記錄返回給客戶端了。因?yàn)檫@個(gè)特殊現(xiàn)象的存在,你也可以認(rèn)為InnoDB中的MVCC并不能完完全全的禁止幻讀。
SERIALIZABLE隔離級(jí)別下,需要分為兩種情況討論:
在系統(tǒng)變量autocommit=0時(shí),也就是禁用自動(dòng)提交時(shí),普通的SELECT語句會(huì)被轉(zhuǎn)為SELECT ... LOCK IN SHARE MODE這樣的語句,也就是在讀取記錄前需要先獲得記錄的S鎖,具體的加鎖情況和REPEATABLE READ隔離級(jí)別下一樣,我們后邊再分析。
在系統(tǒng)變量autocommit=1時(shí),也就是啟用自動(dòng)提交時(shí),普通的SELECT語句并不加鎖,只是利用MVCC來生成一個(gè)ReadView去讀取記錄。
為啥不加鎖呢?因?yàn)閱⒂米詣?dòng)提交意味著一個(gè)事務(wù)中只包含一條語句,一條語句也就沒有啥不可重復(fù)讀、幻讀這樣的問題了。
鎖定讀的語句
我們把下邊四種語句放到一起討論:
語句一:SELECT ... LOCK IN SHARE MODE;
語句二:SELECT ... FOR UPDATE;
語句三:UPDATE ...
語句四:DELETE ...
我們說語句一和語句二是MySQL中規(guī)定的兩種鎖定讀的語法格式,而語句三和語句四由于在執(zhí)行過程需要首先定位到被改動(dòng)的記錄并給記錄加鎖,也可以被認(rèn)為是一種鎖定讀。
READ UNCOMMITTED/READ COMMITTED隔離級(jí)別下
在READ UNCOMMITTED下語句的加鎖方式和READ COMMITTED隔離級(jí)別下語句的加鎖方式基本一致,所以就放到一塊兒說了。值得注意的是,采用加鎖方式解決并發(fā)事務(wù)帶來的問題時(shí),其實(shí)臟讀和不可重復(fù)讀在任何一個(gè)隔離級(jí)別下都不會(huì)發(fā)生(因?yàn)樽x-寫操作需要排隊(duì)進(jìn)行)。
對(duì)于使用主鍵進(jìn)行等值查詢的情況
使用SELECT ... LOCK IN SHARE MODE來為記錄加鎖,比方說:
SELECT * FROM hero WHERE number = 8 LOCK IN SHARE MODE;這個(gè)語句執(zhí)行時(shí)只需要訪問一下聚簇索引中number值為8的記錄,所以只需要給它加一個(gè)S型正經(jīng)記錄鎖就好了,如圖所示:
使用SELECT ... FOR UPDATE來為記錄加鎖,比方說:
SELECT * FROM hero WHERE number = 8 FOR UPDATE;這個(gè)語句執(zhí)行時(shí)只需要訪問一下聚簇索引中number值為8的記錄,所以只需要給它加一個(gè)X型正經(jīng)記錄鎖就好了,如圖所示:
小貼士:為了區(qū)分S鎖和X鎖,我們之后在示意圖中就把加了S鎖的記錄染成藍(lán)色,把加了X鎖的記錄染成紫色。
使用UPDATE ...來為記錄加鎖,比方說:
UPDATE hero SET country = '漢' WHERE number = 8;這條UPDATE語句并沒有更新二級(jí)索引列,加鎖方式和上邊所說的SELECT ... FOR UPDATE語句一致。
如果UPDATE語句中更新了二級(jí)索引列,比方說:
UPDATE hero SET name = 'cao曹操' WHERE number = 8;該語句的實(shí)際執(zhí)行步驟是首先更新對(duì)應(yīng)的number值為8的聚簇索引記錄,再更新對(duì)應(yīng)的二級(jí)索引記錄,所以加鎖的步驟就是:
為number值為8的聚簇索引記錄加上X型正經(jīng)記錄鎖(該記錄對(duì)應(yīng)的)。
為該聚簇索引記錄對(duì)應(yīng)的idx_name二級(jí)索引記錄(也就是name值為'c曹操',number值為8的那條二級(jí)索引記錄)加上X型正經(jīng)記錄鎖。
畫個(gè)圖就是這樣:
小貼士:我們用帶圓圈的數(shù)字來表示為各條記錄加鎖的順序。
使用DELETE ...來為記錄加鎖,比方說:
DELETE FROM hero WHERE number = 8;我們平時(shí)所說的“DELETE表中的一條記錄”其實(shí)意味著對(duì)聚簇索引和所有的二級(jí)索引中對(duì)應(yīng)的記錄做DELETE操作,本例子中就是要先把number值為8的聚簇索引記錄執(zhí)行DELETE操作,然后把對(duì)應(yīng)的idx_name二級(jí)索引記錄刪除,所以加鎖的步驟和上邊更新帶有二級(jí)索引列的UPDATE語句一致,就不畫圖了。
對(duì)于使用主鍵進(jìn)行范圍查詢的情況
使用SELECT ... LOCK IN SHARE MODE來為記錄加鎖,比方說:
SELECT * FROM hero WHERE number <= 8 LOCK IN SHARE MODE;這個(gè)語句看起來十分簡單,但它的執(zhí)行過程還是有一丟丟小復(fù)雜的:
先到聚簇索引中定位到滿足number <= 8的第一條記錄,也就是number值為1的記錄,然后為其加鎖。
判斷一下該記錄是否符合索引條件下推中的條件。
我們前邊介紹過一個(gè)稱之為索引條件下推(?Index Condition Pushdown,簡稱ICP)的功能,也就是把查詢中與被使用索引有關(guān)的查詢條件下推到存儲(chǔ)引擎中判斷,而不是返回到server層再判斷。不過需要注意的是,索引條件下推只是為了減少回表次數(shù),也就是減少讀取完整的聚簇索引記錄的次數(shù),從而減少IO操作。而對(duì)于聚簇索引而言不需要回表,它本身就包含著全部的列,也起不到減少IO操作的作用,所以設(shè)計(jì)InnoDB的大叔們規(guī)定這個(gè)索引條件下推特性只適用于二級(jí)索引。也就是說在本例中與被使用索引有關(guān)的條件是:number <= 8,而number列又是聚簇索引列,所以本例中并沒有符合索引條件下推的查詢條件,自然也就不需要判斷該記錄是否符合索引條件下推中的條件。
判斷一下該記錄是否符合范圍查詢的邊界條件
因?yàn)樵诒纠惺抢弥麈Inumber進(jìn)行范圍查詢,設(shè)計(jì)InnoDB的大叔規(guī)定每從聚簇索引中取出一條記錄時(shí)都要判斷一下該記錄是否符合范圍查詢的邊界條件,也就是number <= 8這個(gè)條件。如果符合的話將其返回給server層繼續(xù)處理,否則的話需要釋放掉在該記錄上加的鎖,并給server層返回一個(gè)查詢完畢的信息。
對(duì)于number值為1的記錄是符合這個(gè)條件的,所以會(huì)將其返回到server層繼續(xù)處理。
將該記錄返回到server層繼續(xù)判斷。
server層如果收到存儲(chǔ)引擎層提供的查詢完畢的信息,就結(jié)束查詢,否則繼續(xù)判斷那些沒有進(jìn)行索引條件下推的條件,在本例中就是繼續(xù)判斷number <= 8這個(gè)條件是否成立。噫,不是在第3步中已經(jīng)判斷過了么,怎么在這又判斷一回?是的,設(shè)計(jì)InnoDB的大叔采用的策略就是這么簡單粗暴,把凡是沒有經(jīng)過索引條件下推的條件都需要放到server層再判斷一遍。如果該記錄符合剩余的條件(沒有進(jìn)行索引條件下推的條件),那么就把它發(fā)送給客戶端,不然的話需要釋放掉在該記錄上加的鎖。
然后剛剛查詢得到的這條記錄(也就是number值為1的記錄)組成的單向鏈表繼續(xù)向后查找,得到了number值為3的記錄,然后重復(fù)第2,3,4、5這幾個(gè)步驟。
小貼士:上述步驟是在MySQL 5.7.21這個(gè)版本中驗(yàn)證的,不保證其他版本有無出入。
但是這個(gè)過程有個(gè)問題,就是當(dāng)找到number值為8的那條記錄的時(shí)候,還得向后找一條記錄(也就是number值為15的記錄),在存儲(chǔ)引擎讀取這條記錄的時(shí)候,也就是上述的第1步中,就得為這條記錄加鎖,然后在第3步時(shí),判斷該記錄不符合number <= 8這個(gè)條件,又要釋放掉這條記錄的鎖,這個(gè)過程導(dǎo)致number值為15的記錄先被加鎖,然后把鎖釋放掉,過程就是這樣:
這個(gè)過程有意思的一點(diǎn)就是,如果你先在事務(wù)T1中執(zhí)行:
# 事務(wù)T1BEGIN;
SELECT * FROM hero WHERE number <= 8 LOCK IN SHARE MODE;
然后再到事務(wù)T2中執(zhí)行:
# 事務(wù)T2BEGIN;
SELECT * FROM hero WHERE number = 15 FOR UPDATE;
是沒有問題的,因?yàn)樵赥2執(zhí)行時(shí),事務(wù)T1已經(jīng)釋放掉了number值為15的記錄的鎖,但是如果你先執(zhí)行T2,再執(zhí)行T1,由于T2已經(jīng)持有了number值為15的記錄的鎖,事務(wù)T1將因?yàn)楂@取不到這個(gè)鎖而等待。
我們?cè)倏匆粋€(gè)使用主鍵進(jìn)行范圍查詢的例子:
SELECT * FROM hero WHERE number >= 8 LOCK IN SHARE MODE;這個(gè)語句的執(zhí)行過程其實(shí)和我們舉的上一個(gè)例子類似。也是先到聚簇索引中定位到滿足number >= 8這個(gè)條件的第一條記錄,也就是number值為8的記錄,然后就可以沿著由記錄組成的單向鏈表一路向后找,每找到一條記錄,就會(huì)為其加上鎖,然后判斷該記錄符不符合范圍查詢的邊界條件,不過這里的邊界條件比較特殊:number >= 8,只要記錄不小于8就算符合邊界條件,所以判斷和沒判斷是一樣一樣的。最后把這條記錄返回給server層,server層再判斷number >= 8這個(gè)條件是否成立,如果成立的話就發(fā)送給客戶端,否則的話就結(jié)束查詢。不過InnoDB存儲(chǔ)引擎找到索引中的最后一條記錄,也就是Supremum偽記錄之后,在存儲(chǔ)引擎內(nèi)部就可以立即判斷這是一條偽記錄,不必要返回給server層處理,也沒必要給它也加上鎖(也就是說在第1步中就壓根兒沒給這條記錄加鎖)。整個(gè)過程會(huì)給number值為8、15、20這三條記錄加上S型正經(jīng)記錄鎖,畫個(gè)圖表示一下就是這樣:
使用SELECT ... FOR UPDATE語句來為記錄加鎖:
和SELECT ... FOR UPDATE語句類似,只不過加的是X型正經(jīng)記錄鎖。
使用UPDATE ...來為記錄加鎖,比方說:
UPDATE hero SET country = '漢' WHERE number >= 8;這條UPDATE語句并沒有更新二級(jí)索引列,加鎖方式和上邊所說的SELECT ... FOR UPDATE語句一致。
如果UPDATE語句中更新了二級(jí)索引列,比方說:
UPDATE hero SET name = 'cao曹操' WHERE number >= 8;這時(shí)候會(huì)首先更新聚簇索引記錄,再更新對(duì)應(yīng)的二級(jí)索引記錄,所以加鎖的步驟就是:
為number值為8的聚簇索引記錄加上X型正經(jīng)記錄鎖。
然后為上一步中的記錄索引記錄對(duì)應(yīng)的idx_name二級(jí)索引記錄加上X型正經(jīng)記錄鎖。
為number值為15的聚簇索引記錄加上X型正經(jīng)記錄鎖。
然后為上一步中的記錄索引記錄對(duì)應(yīng)的idx_name二級(jí)索引記錄加上X型正經(jīng)記錄鎖。
為number值為20的聚簇索引記錄加上X型正經(jīng)記錄鎖。
然后為上一步中的記錄索引記錄對(duì)應(yīng)的idx_name二級(jí)索引記錄加上X型正經(jīng)記錄鎖。
畫個(gè)圖就是這樣:
如果是下邊這個(gè)語句:
UPDATE hero SET namey = '漢' WHERE number <= 8;則會(huì)對(duì)number值為1、3、8聚簇索引記錄以及它們對(duì)應(yīng)的二級(jí)索引記錄加X型正經(jīng)記錄鎖,加鎖順序和上邊語句中的加鎖順序類似,都是先對(duì)一條聚簇索引記錄加鎖后,再給對(duì)應(yīng)的二級(jí)索引記錄加鎖。之后會(huì)繼續(xù)對(duì)number值為15的聚簇索引記錄加鎖,但是隨后InnoDB存儲(chǔ)引擎判斷它不符合邊界條件,隨即會(huì)釋放掉該聚簇索引記錄上的鎖(注意這個(gè)過程中沒有對(duì)number值為15的聚簇索引記錄對(duì)應(yīng)的二級(jí)索引記錄加鎖)。具體示意圖就不畫了。
使用DELETE ...來為記錄加鎖,比方說:
DELETE FROM hero WHERE number >= 8;和
DELETE FROM hero WHERE number <= 8;這兩個(gè)語句的加鎖情況和更新帶有二級(jí)索引列的UPDATE語句一致,就不畫圖了。
對(duì)于使用二級(jí)索引進(jìn)行等值查詢的情況
小貼士:在READ UNCOMMITTED和READ COMMITTED隔離級(jí)別下,使用普通的二級(jí)索引和唯一二級(jí)索引進(jìn)行加鎖的過程是一樣的,所以我們也就不分開討論了。
使用SELECT ... LOCK IN SHARE MODE來為記錄加鎖,比方說:
SELECT * FROM hero WHERE name = 'c曹操' LOCK IN SHARE MODE;這個(gè)語句的執(zhí)行過程是先通過二級(jí)索引idx_name定位到滿足name = 'c曹操'條件的二級(jí)索引記錄,然后進(jìn)行回表操作。所以先要對(duì)二級(jí)索引記錄加S型正經(jīng)記錄鎖,然后再給對(duì)應(yīng)的聚簇索引記錄加S型正經(jīng)記錄鎖,示意圖如下:
這里需要再次強(qiáng)調(diào)一下這個(gè)語句的加鎖順序:
先對(duì)name列為'c曹操'二級(jí)索引記錄進(jìn)行加鎖。
再對(duì)相應(yīng)的聚簇索引記錄進(jìn)行加鎖
小貼士:我們知道idx_name是一個(gè)普通的二級(jí)索引,到idx_name索引中定位到滿足name= 'c曹操'這個(gè)條件的第一條記錄后,就可以沿著這條記錄一路向后找。可是從我們上邊的描述中可以看出來,并沒有對(duì)下一條二級(jí)索引記錄進(jìn)行加鎖,這是為什么呢?這是因?yàn)樵O(shè)計(jì)InnoDB的大叔對(duì)等值匹配的條件有特殊處理,他們規(guī)定在InnoDB存儲(chǔ)引擎層查找到當(dāng)前記錄的下一條記錄時(shí),在對(duì)其加鎖前就直接判斷該記錄是否滿足等值匹配的條件,如果不滿足直接返回(也就是不加鎖了),否則的話需要將其加鎖后再返回給server層。所以這里也就不需要對(duì)下一條二級(jí)索引記錄進(jìn)行加鎖了。
現(xiàn)在要介紹一個(gè)非常有趣的事情,我們假設(shè)上邊這個(gè)語句在事務(wù)T1中運(yùn)行,然后事務(wù)T2中運(yùn)行下邊一個(gè)我們之前介紹過的語句:
UPDATE hero SET name = '曹操' WHERE number = 8;這兩個(gè)語句都是要對(duì)number值為8的聚簇索引記錄和對(duì)應(yīng)的二級(jí)索引記錄加鎖,但是不同點(diǎn)是加鎖的順序不一樣。這個(gè)UPDATE語句是先對(duì)聚簇索引記錄進(jìn)行加鎖,后對(duì)二級(jí)索引記錄進(jìn)行加鎖,如果在不同事務(wù)中運(yùn)行上述兩個(gè)語句,可能發(fā)生一種賊奇妙的事情 ——
事務(wù)T2持有了聚簇索引記錄的鎖,事務(wù)T1持有了二級(jí)索引記錄的鎖。
事務(wù)T2在等待獲取二級(jí)索引記錄上的鎖,事務(wù)T1在等待獲取聚簇索引記錄上的鎖。
兩個(gè)事務(wù)都分別持有一個(gè)鎖,而且都在等待對(duì)方已經(jīng)持有的那個(gè)鎖,這種情況就是所謂的死鎖,兩個(gè)事務(wù)都無法運(yùn)行下去,必須選擇一個(gè)進(jìn)行回滾,對(duì)性能影響比較大。??
使用SELECT ... FOR UPDATE語句時(shí),比如:
SELECT * FROM hero WHERE name = 'c曹操' FOR UPDATE;這種情況下與SELECT ... LOCK IN SHARE MODE語句的加鎖情況類似,都是給訪問到的二級(jí)索引記錄和對(duì)應(yīng)的聚簇索引記錄加鎖,只不過加的是X型正經(jīng)記錄鎖罷了。
使用UPDATE ...來為記錄加鎖,比方說:
與更新二級(jí)索引記錄的SELECT ... FOR UPDATE的加鎖情況類似,不過如果被更新的列中還有別的二級(jí)索引列的話,對(duì)應(yīng)的二級(jí)索引記錄也會(huì)被加鎖。
使用DELETE ...來為記錄加鎖,比方說:
與SELECT ... FOR UPDATE的加鎖情況類似,不過如果表中還有別的二級(jí)索引列的話,對(duì)應(yīng)的二級(jí)索引記錄也會(huì)被加鎖。
對(duì)于使用二級(jí)索引進(jìn)行范圍查詢的情況
使用SELECT ... LOCK IN SHARE MODE來為記錄加鎖,比方說:
SELECT * FROM hero FORCE INDEX(idx_name) WHERE name >= 'c曹操' LOCK IN SHARE MODE;小貼士:因?yàn)閮?yōu)化器會(huì)計(jì)算使用二級(jí)索引進(jìn)行查詢的成本,在成本較大時(shí)可能選擇以全表掃描的方式來執(zhí)行查詢,所以我們這里使用FORCE INDEX(idx_name)來強(qiáng)制使用二級(jí)索引idx_name來執(zhí)行查詢。
這個(gè)語句的執(zhí)行過程其實(shí)是先到二級(jí)索引中定位到滿足name >= 'c曹操'的第一條記錄,也就是name值為c曹操的記錄,然后就可以沿著這條記錄的鏈表一路向后找,從二級(jí)索引idx_name的示意圖中可以看出,所有的用戶記錄都滿足name >= 'c曹操'的這個(gè)條件,所以所有的二級(jí)索引記錄都會(huì)被加S型正經(jīng)記錄鎖,它們對(duì)應(yīng)的聚簇索引記錄也會(huì)被加S型正經(jīng)記錄鎖。不過需要注意一下加鎖順序,對(duì)一條二級(jí)索引記錄加鎖完后,會(huì)接著對(duì)它相應(yīng)的聚簇索引記錄加鎖,完后才會(huì)對(duì)下一條二級(jí)索引記錄進(jìn)行加鎖,以此類推~ 畫個(gè)圖表示一下就是這樣:
再來看下邊這個(gè)語句:
SELECT * FROM hero FORCE INDEX(idx_name) WHERE name <= 'c曹操' LOCK IN SHARE MODE;這個(gè)語句的加鎖情況就有點(diǎn)兒有趣了。前邊說在使用number <= 8這個(gè)條件的語句中,需要把number值為15的記錄也加一個(gè)鎖,之后又判斷它不符合邊界條件而把鎖釋放掉。而對(duì)于查詢條件name <= 'c曹操'的語句來說,執(zhí)行該語句需要使用到二級(jí)索引,而與二級(jí)索引相關(guān)的條件是可以使用索引條件下推這個(gè)特性的。設(shè)計(jì)InnoDB的大叔規(guī)定,如果一條記錄不符合索引條件下推中的條件的話,直接跳到下一條記錄(這個(gè)過程根本不將其返回到server層),如果這已經(jīng)是最后一條記錄,那么直接向server層報(bào)告查詢完畢。但是這里頭有個(gè)問題呀:先對(duì)一條記錄加了鎖,然后再判斷該記錄是不是符合索引條件下推的條件,如果不符合直接跳到下一條記錄或者直接向server層報(bào)告查詢完畢,這個(gè)過程中并沒有把那條被加鎖的記錄上的鎖釋放掉呀!!!。本例中使用的查詢條件是name <= 'c曹操',在為name值為'c曹操'的二級(jí)索引記錄以及它對(duì)應(yīng)的聚簇索引加鎖之后,會(huì)接著二級(jí)索引中的下一條記錄,也就是name值為'l劉備'的那條二級(jí)索引記錄,由于該記錄不符合索引條件下推的條件,而且是范圍查詢的最后一條記錄,會(huì)直接向server層報(bào)告查詢完畢,重點(diǎn)是這個(gè)過程中并不會(huì)釋放name值為'l劉備'的二級(jí)索引記錄上的鎖,也就導(dǎo)致了語句執(zhí)行完畢時(shí)的加鎖情況如下所示:
這樣子會(huì)造成一個(gè)尷尬情況,假如T1執(zhí)行了上述語句并且尚未提交,T2再執(zhí)行這個(gè)語句:
SELECT * FROM hero WHERE name = 'l劉備' FOR UPDATE;T2中的語句需要獲取name值為l劉備的二級(jí)索引記錄上的X型正經(jīng)記錄鎖,而T1中仍然持有name值為l劉備的二級(jí)索引記錄上的S型正經(jīng)記錄鎖,這就造成了T2獲取不到鎖而進(jìn)入等待狀態(tài)。
小貼士:為啥不能釋放不符合索引條件下推中的條件的二級(jí)索引記錄上的鎖呢?這個(gè)問題我也沒想明白,人家就是這么規(guī)定的,如果有明白的小伙伴可以加我微信 xiaohaizi4919 來討論一下哈~ 再強(qiáng)調(diào)一下,我使用的MySQL版本是5.7.21,不保證其他版本中的加鎖情景是否完全一致。
使用SELECT ... FOR UPDATE語句時(shí):
和SELECT ... FOR UPDATE語句類似,只不過加的是X型正經(jīng)記錄鎖。
使用UPDATE ...來為記錄加鎖,比方說:
UPDATE hero SET country = '漢' WHERE name >= 'c曹操';小貼士:FORCE INDEX只對(duì)SELECT語句起作用,UPDATE語句雖然支持該語法,但實(shí)質(zhì)上不起作用,DELETE語句壓根兒不支持該語法。
假設(shè)該語句執(zhí)行時(shí)使用了idx_name二級(jí)索引來進(jìn)行鎖定讀,那么它的加鎖方式和上邊所說的SELECT ... FOR UPDATE語句一致。如果有其他二級(jí)索引列也被更新,那么也會(huì)為對(duì)應(yīng)的二級(jí)索引記錄進(jìn)行加鎖,就不贅述了。不過還有一個(gè)有趣的情況,比方說:
UPDATE hero SET country = '漢' WHERE name <= 'c曹操';我們前邊說的索引條件下推這個(gè)特性只適用于SELECT語句,也就是說UPDATE語句中無法使用,那么這個(gè)語句就會(huì)為name值為'c曹操'和'l劉備'的二級(jí)索引記錄以及它們對(duì)應(yīng)的聚簇索引進(jìn)行加鎖,之后在判斷邊界條件時(shí)發(fā)現(xiàn)name值為'l劉備'的二級(jí)索引記錄不符合name <= 'c曹操'條件,再把該二級(jí)索引記錄和對(duì)應(yīng)的聚簇索引記錄上的鎖釋放掉。這個(gè)過程如下圖所示:
使用DELETE ...來為記錄加鎖,比方說:
DELETE FROM hero WHERE name >= 'c曹操';和
DELETE FROM hero WHERE name <= 'c曹操';如果這兩個(gè)語句采用二級(jí)索引來進(jìn)行鎖定讀,那么它們的加鎖情況和更新帶有二級(jí)索引列的UPDATE語句一致,就不畫圖了。
全表掃描的情況
比方說:
SELECT * FROM hero WHERE country = '魏' LOCK IN SHARE MODE;由于country列上未建索引,所以只能采用全表掃描的方式來執(zhí)行這條查詢語句,存儲(chǔ)引擎每讀取一條聚簇索引記錄,就會(huì)為這條記錄加鎖一個(gè)S型正常記錄鎖,然后返回給server層,如果server層判斷country = '魏'這個(gè)條件是否成立,如果成立則將其發(fā)送給客戶端,否則會(huì)釋放掉該記錄上的鎖,畫個(gè)圖就像這樣:
使用SELECT ... FOR UPDATE進(jìn)行加鎖的情況與上邊類似,只不過加的是X型正經(jīng)記錄鎖,就不贅述了。
對(duì)于UPDATE ...和DELETE ...的語句來說,在遍歷聚簇索引中的記錄,都會(huì)為該聚簇索引記錄加上X型正經(jīng)記錄鎖,然后:
如果該聚簇索引記錄不滿足條件,直接把該記錄上的鎖釋放掉。
如果該聚簇索引記錄滿足條件,則會(huì)對(duì)相應(yīng)的二級(jí)索引記錄加上X型正經(jīng)記錄鎖(DELETE語句會(huì)對(duì)所有二級(jí)索引列加鎖,UPDATE語句只會(huì)為更新的二級(jí)索引列對(duì)應(yīng)的二級(jí)索引記錄加鎖)。
推薦閱讀
1、徹底搞懂StringBuffer和StringBuilder的區(qū)別
2、徒手?jǐn)]一個(gè)Spring MVC框架
3、徒手?jǐn)]一個(gè)MyBatis框架
4、B站視頻上新,?我又?jǐn)]了一套干貨滿滿的教程滿
總結(jié)
以上是生活随笔為你收集整理的c++客户端发送加锁_MySQL语句加锁分析详解的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 成都欢乐谷激流勇进多高
- 下一篇: 不孕不育和孕前检查有什么不同