mysql 鎖,和加鎖機制

背景
間隙鎖是MySQL在RR可重複讀隔離級別下用來修復幻讀才引入的一種鎖,間隙鎖也只有在RR可重複讀隔離級別下才會存在,如果是在RC讀已提交隔離級別下,是沒有間隙鎖的存在的。另外,我們也知道,幻讀這種現象也只有在當前讀的時候纔會發生,在一致性快照讀的情況下是沒有幻讀現象的。

那麼間隙鎖到底是怎麼樣工作的?它是如何保證在當前讀的時候,不會出現幻讀現象的呢?接下來讓我們一起剖解分享一下間隙的加鎖的機制是怎麼樣的。

實驗環境的準備
前置條件
接下來的實驗是在如下的環境下開始的:

MySQL的版本:5.7.24
事務的隔離級別:RR可重複讀
測試使用存儲引擎爲innodb存儲引擎
準備建表語句
下面的實驗使用的userinfo表結果如下表格所示:其中用戶的年齡age字段是可能存在多個用戶的年齡相同的情況,所以這個age列上有非唯一索引;而所有用戶的手機號碼不會重複,所以這個phone列上面有一個唯一索引;姓名name列和備註remark列上面沒有任何索引就是兩個普通的字段。

序號 字段名稱 字段類型 字段註釋 索引類型

序號字段名稱字段類型字段註釋索引類型
1 id int 表的主鍵 聚簇索引
2 name varchar 姓名 N/A
3 age int 年齡 非唯一索引
4 phone varchar 手機號 唯一索引
5 remark varchar 備註信息 N/A


建表語句如下:

CREATE TABLE `userinfo` (
  `id` int(11) NOT NULL COMMENT '主鍵',
  `name` varchar(255) DEFAULT NULL COMMENT '姓名',
  `age` int(11) DEFAULT NULL COMMENT '年齡,普通索引列',
  `phone` varchar(255) DEFAULT NULL COMMENT '手機,唯一索引列',
  `remark` varchar(255) DEFAULT NULL COMMENT '備註',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_userinfo_phone` (`phone`) USING BTREE COMMENT '手機號碼,唯一索引',
  KEY `idx_user_info_age` (`age`) USING BTREE COMMENT '年齡,普通索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

  


準備初始化數據
下面實驗中使用到的測試數據如下:

T INTO `userinfo`(`id`, `name`, `age`, `phone`, `remark`) VALUES (0, 'mayun', 20, '0000', '馬雲');
INSERT INTO `userinfo`(`id`, `name`, `age`, `phone`, `remark`) VALUES (5, 'liuqiangdong', 23, '5555', '劉強東');
INSERT INTO `userinfo`(`id`, `name`, `age`, `phone`, `remark`) VALUES (10, 'mahuateng', 18, '1010', '馬化騰');
INSERT INTO `userinfo`(`id`, `name`, `age`, `phone`, `remark`) VALUES (15, 'liyanhong', 27, '1515', '李彥宏');
INSERT INTO `userinfo`(`id`, `name`, `age`, `phone`, `remark`) VALUES (20, 'wangxing', 23, '2020', '王興');
INSERT INTO `userinfo`(`id`, `name`, `age`, `phone`, `remark`) VALUES (25, 'zhangyiming', 38, '2525', '張一鳴');

 

最後的測試環境
準備好表和初始化數據之後,我們的測試環境就準備好了,最後的測試環境如下:

 


間隙鎖的結構
針對上面我們準備表結構和插入的測試數據,目前userinfo表中有三個間隙鎖,分別在主鍵id列上,非唯一索引age列,唯一索引phone列。它們的間隙鎖的分佈情況分別如下。根據各個索引列上面的值,把索引切分爲不同的區間段。

主鍵索引id列上的間隙鎖結構如下圖所示:

 

非唯一索引age列上的間隙鎖結構如下所示:因爲是非因爲索引,所以索引中的值可以重複出現,所以在圖中沒有標記每一個間隙可能出現的值,用三個點代替顯示。

 

唯一索引phone列上的間隙鎖如下所示:

 

根據主鍵查詢,給行增加X鎖

我們要知道,MySQL中的行鎖和間隙鎖是鎖定就是對應的索引。行鎖鎖定的行所在的主鍵索引,非主鍵索引列上面的鎖也是鎖定對應的非主鍵索引。間隙鎖也是鎖定索引,他們不是鎖定行,也不是鎖定某個列,是鎖定對應的索引。

索引的結構
上面的表插入數據之後,在id主鍵索引上會有一個B+Tree的索引結構。在age非唯一索引和phone唯一索引兩列上,會有兩個B+Tree索引結構。他們的結構如下圖所示:

id主鍵索引結構

 

 


age非唯一索引結

 


phone唯一索引

 


間隙鎖加鎖規則
這裏先簡單總結一下間隙鎖加鎖的一些規則,然後我們根據規則去逐步驗證這些規則。

  1. 查詢過程中訪問到的對象纔會加鎖。
  2. 加鎖的基本單位是next-key lock(前開後閉)。
  3. 等值查詢上MySQL的優化:索引上的等值查詢,如果是唯一索引,next-key lock會退化爲行鎖,如果不是唯一索引,需要訪問到第一個不滿足條件的值,此時next-key lock會退化爲間隙鎖。
  4. 範圍查詢:無論是否是唯一索引,範圍查詢都需要訪問到不滿足條件的第一個值爲止。

實驗部分

我們針對以下幾個SQL語句來分析一下具體間隙鎖的加鎖的範圍是什麼。這幾個SQL分別使用不同的列來作爲where條件來篩選表中第2條數據。

根據where條件中各個列的類型,我們可以分爲如下4類SQL。下面的四個語句都會自動給表增加上對應的行鎖之外,如果有必要增加對應的間隙鎖,也會增加上間隙鎖來避免幻讀的發生,而如果不需要間隙鎖就可以避免幻讀的發生,那麼MySQL就不會自動增加上對應的間隙鎖只要對應的行鎖就可以了。

/*根據主鍵查詢,給行增加X鎖*/
select * from userinfo where id = 5 for update;

/*根據唯一索引列查詢,給行增加X鎖*/
select * from userinfo where phone = '5555' for update;

/*根據非唯一索引列查詢,給行增加X鎖*/
select * from userinfo where age = 23;

/*根據普通列查詢,給行增加X鎖*/
select * from userinfo where name = 'liuqiangdong';

 


實驗的過程中,還需要密切觀察下面三個表中的內容,這裏記錄的MySQL在執行期間正在允許的事務、鎖、鎖等待等信息。從這裏三張表中,可以看到每一個阻塞是因爲什麼阻塞的,被哪些鎖阻塞的。

/*查看正在運行的事務*/
select * from information_schema.innodb_trx;

/*查看當前的鎖信息*/
select * from information_schema.innodb_locks;

/*查看鎖等待的信息*/
select * from information_schema.innodb_lock_waits;

 

RR級別+主鍵索引列
在可重複讀隔離級別下,通過主鍵索引去查詢數據嘗試增加X鎖的時候,使用如下的SQL語句

/*根據主鍵查詢,給行增加X鎖*/
select * from userinfo where id = 5 for update;

 

此時會在主鍵索引上索引值爲5的記錄上增加X鎖,此時不需要使用其他間隙鎖就可以避免幻讀的發生。因爲主鍵索引是唯一索引,當鎖住這一行數據後,其他事務將不能做如下操作:

  1. 不能刪除id=5的這一行數據。id=5的行已經被當前事務給增加了X鎖,所以其他事務將不能查詢、修改、刪除這一行數據。
  2. 不能新插入一個id=5的行。表中已經存在主鍵id=5的行了,所以其他事務不能再次增加一個id=5的行。所以就可以避免在id=5這個條件下再次查詢的時候出現多行數據而產生幻讀的現象。
  3. 不能修改id=5的這一行數據。id=5的行已經被當前事務給增加了X鎖,所以其他事務將不能查詢、修改、刪除這一行數據。
  4. 不能把其他行的id值改爲5。這一行是主鍵索引,也是唯一所以,它的值不能重複。所以其他事務不能把一個id!=5的行改爲id=5的行。所以就可以避免在id=5這個條件下再次查詢的時候出現多行數據而出現幻讀的現象。

此時增加的鎖也只有主鍵索引id=5這一個行鎖,鎖結構如下:

 


綜上幾點可以確定,當使用主鍵索引進行查詢數據增加X鎖的時候,是可以避免幻讀的發生,此時不需要間隙鎖的參與就可以避免幻讀。其他事務可以正常的對userinfo表進行除id=5之外增刪改查操作。比如插入一個id=6的數據行,刪除id=0的數據行,修改id=20的行等操作。

實驗截圖:

通過如下的實驗,可以看出,在左側的事務給表userinfo增加了id=5的行鎖之後,右側的事務仍然可以對錶中其他數據行和間隙進行增刪改查。說明左側的事務只增加了id=5的行鎖,沒有間隙鎖的存在。

 


下面我們再來看一個因爲索引失效而走全表掃描的例子,與此同時下面的這個例子也能從一定程度上說明左側的事務只對id=5這一行增加了一個X鎖,沒有其他間隙鎖或行鎖的存在。

 


上面截圖中的第5,6,7,8在嘗試加S鎖的時候,這裏解釋一下爲什麼成功,爲什麼失敗。

  • 第5步中,是通過id=0這個條件去給id=0的這一行數據增加S共享鎖,where條件中的id是主鍵列,也是一個唯一索引列,所以在查詢的時候,可以直接通過主鍵索引定位到對應的行。所以直接找到了id=0的這一行,同時發現這一行上面沒有其他X所存在,所以增加S鎖成功。
  • 第6步中,是通過phone=0000這個條件去給id=0的行增加S鎖,這裏之所以失敗的原因如下:
  1. phone是一個varchar類型的唯一索引,在給它賦值的時候,如果我們賦值不是varchar類型的數據,MySQL爲了避免SQL直接出現錯誤,會嘗試進行隱式轉換,把數據庫中的phone列使用函數轉換爲和你賦值的類型一致的數據再進行等值判斷。
  2. 對索引列使用函數操作,會導致查詢的時候不走索引,索引對當前查詢SQL語句失效了,此時的phone的索引就不會被使用,所以此時在查詢數據的時候就是走全表掃描。
  3. 在全表掃描的時候,掃描到第一個phone=0000的行並不會停止,因爲此時phone列的唯一索引沒有使用,所以要繼續掃描,判斷表中每一行的phone列的值是否爲0000。根據前面說的加鎖規則,在嘗試加鎖的時候,會對所有掃描過的對象增加對應的鎖。所以在掃描到id=5的行的時候,嘗試給這一行增加S鎖。
  4. 然而,id=5的這一行的索引記錄,已經被左側的事務給增加了一個X鎖,因爲S鎖和X鎖不能共存,所以此時給id=5的索引記錄加S鎖失敗。
  5. 所以步驟6就被阻塞住了,等超過事務默認的最大等待閾值就會退出。
  • 第7步中,執行成功了的原因正式因爲我們在給phone列賦值的時候,使用了正確的varchar類型的'0000',所以在查詢表中數據的時候,可以使用到唯一索引,直接定位到對應的主鍵索引上面的值,從而可以對id=0的索引記錄加鎖成功。不需要走全表掃描就可以找到對應的行。
  • 第8步中,被阻塞的原因是因爲它要給id=5的索引記錄增加S鎖,它可以通過主鍵索引直接定位到要加S鎖的索引記錄行,不需要走全表掃描。找到id=5的行後嘗試給它加S鎖,但是發現這個行已經被左側的事務給增加了X鎖。S鎖和X鎖不能共存,所以右側的事務給id=5的行增加S鎖失敗。

所以:在我們平時開發的時候,寫SQL語句的時候,一定要格外的注意給where條件後面的字段賦值的時候,一定要根據對應的字段類型進行賦值。切不要讓MySQL使用隱式轉換的功能導致索引失效而走全表掃描。這樣我們會有一種錯覺:爲啥我加了索引了,查詢還是很慢。你要保證:增加了索引,並且SQL語句查詢的時候真正使用到了索引,這樣纔會對你的SQL性能有提升。

RR級別+唯一索引列
當我們嘗試通過一個唯一索引列去給表增加X鎖的時候,會使用如下的SQL,它會給表增加那些鎖呢?

/*根據唯一索引列查詢,給行增加X鎖*/
select * from userinfo where phone = '5555' for update;

 

此時查詢數據的過程是這樣的:先查唯一索引,然後再回表查詢主鍵索引,然後從主鍵索引上返回查詢的結果。具體流執行流程如下:

先根據phone列上面的唯一索引找到索引值爲’5555’索引記錄,因爲是唯一索引,所以找到5555的索引記錄後就停止搜索了,索引樹上只有一條5555的記錄。
然後再根據這個索引記錄上面存儲的主鍵索引的值5去主鍵索引上面查找需要查詢的行記錄。
根據加鎖規則中提到的:只會在掃描到的對象上增加鎖。所以會在主鍵索引id=5的索引記錄上增加X鎖,也會在唯一索引phone='5555’的索引記錄上增加一個X鎖。
此時加鎖的情況如下所示:


疑問1:如何證明通過select * from userinfo where phone='5555' for update增加X鎖的時候,在索引列phone='5555’的索引記錄上增加了X鎖了?通過如下實驗可以證明這個結論:

 


針對上圖中的各個實驗步驟,簡單做如下說明。

  • 在第3步執行完成的之後,在非主鍵索引(非聚簇索引、二級索引)phone中的phone='5555’索引記錄行增加了X鎖,同時在主鍵索引(聚簇索引)id中id=5的索引記錄行增加了X鎖。
  • 第4步中查詢語句使用了覆蓋索引的功能,我們只查詢的phone這一列的值,在非主鍵索引上就包含了我們要查詢的結果,所以這個查詢不會去查詢主鍵索引,只會在非主鍵索引上搜索查找。但是它增加S鎖和X鎖都失敗了,說明這個phone='5555’索引記錄上已經被增加了X鎖。
  • 在第5不執行完成後,左側的事務就回滾了,所以在第3步中增加的兩把X鎖都被釋放掉了。
  • 所以在第6步和第7步中,再次嘗試給phone='5555’的非主鍵索引記錄行增加S鎖或X鎖的時候,都加鎖成功了。
  • 此時就已經說明了在通過非主鍵索引且是唯一索引列,在給表增加X鎖的時候,除了會對主鍵索引對應的記錄行增加X鎖之外,還會在非聚簇索引的索引記錄上增加X鎖。
  • 在第11步中,我們做了和第3不類似的操作,但是和第3步的區別在於:此時我們給表增加的是S鎖而不是第3步中的X鎖。
  • 因爲S鎖和S鎖是共享的,所以在右側事務中的第12步中,給phone='5555’的索引記錄增加S鎖,成功。
  • 因爲S鎖和X鎖不能共存,所以在右側事務中的第13步中,給phone='5555’的索引記錄增X鎖,加鎖失敗。而當左側事務執行完第14步回滾事務的操作之後,此時phone列上面的phone值爲’5555’的索引記錄的S鎖已經被釋放,所以做右側事務中,再次嘗試給phone='5555’的索引記錄增加X鎖,此時才成功。

疑問2:爲什麼要在主鍵索引id=5這個記錄上也增加X鎖?如果併發的一個SQL,是通過主鍵索引來刪除數據,SQL語句爲:delete from userinfo where id = 5;。 此時,如果update語句沒有將主鍵索引上的記錄加鎖,那麼併發的delete就會感知不到前面的update語句的存在,違背了同一記錄上的更新/刪除需要串行執行的原則。

疑問3:通過疑問2,我們知道在唯一索引phone上面增加了X鎖,那麼我們前面的實驗一中,通過select * from userinfo where id = 5 for update增加X鎖的時候,有沒有在索引列phone='5555’的索引記錄上增X鎖呢?

我們的實驗截圖如下:

 


注意:目前我只能確定phone='5555’的索引記錄上一定是沒有X鎖,但是有沒有S鎖目前還不能確定,因爲如果通過id=5 for update加X鎖後,如果在phone='5555’的非主鍵索引的索引記錄上增加一個S鎖,在右側的事務中,也是可以獲得S鎖的,因爲S鎖和S鎖是可以共存的。事實上,我們也確實在右側的事務中獲得了phone='5555’索引記錄上的S鎖。索引我不確定在左側的事務中,是否給phone='5555’的索引記錄上增加了S鎖。

如果大家有辦法可以證明這一點,也希望大家給我留言你的證明方式。

RR級別+非唯一索引列
通過非唯一索引列age來增加X鎖的SQL語句如下,當執行完成如下語句之後,表userinfo會有哪些鎖產生呢?

select * from userinfo where age = 23 for update;

加鎖情況如下圖所示:


這裏需要說明幾點:

  • 根據非唯一索引加鎖規則,此次增加的間隙鎖有:(20,23]、(23,27]。對於非唯一索引來說,當掃描到最後一個邊界age=27的時候,臨鍵鎖退化爲間隙鎖。所以此時的鎖爲間隙鎖:(20,27),不包含左右兩邊的邊界值20和27。
  • 行鎖有:在age=23的非主鍵索引記錄上有兩把,因爲有兩個age=23索引記錄。同時在id=5和id=20的主鍵索引記錄上也有兩把X鎖,
  • 只要當age的取值範圍爲(20,27)的時,任何記錄行都不能插入成功。因爲age上面的間隙鎖阻止了數據的插入。
  • 只要當age的值不屬於(20,27),且不等於20也不等於27的時候,任何記錄行都可以插入成功。前提是待插入的數據行中的id的值符合主鍵id的唯一性和phone的值符合phone唯一索引的唯一性。
  • 當age=20的時候,並不是說,id的值可以使用任意表中不存在的主鍵值作爲待插入的主鍵值。此時id的值必須爲小於0的數,不能大於0。舉例說明:當待插入的數據中的age=20, id=1,那麼在非唯一索引age上面存這一行索引記錄的時候,會在上圖中的藍色方框20後面在創建一個age=20的記錄,還需要爲這個非唯一索引下面村上主鍵索引(聚簇索引)的值:1,此時的1比表中已經存在的主鍵值0大,所以它會放在0的後面,如上圖中所示此時的1會佔用被鎖住的索引間隙。所以此時不能插入成功。能插入成功的爲:age=20, id=-1或age=20, id=-2這樣的組合。
  • 當age=27的時候,同理,id的值需要大於15纔可以插入成功。比如:age=27, id=16或者age=27, id=17這樣的組合。如果此時插入一個age=27, id=14的數據行,你設想一下會怎麼存放這個索引記錄,索引中已經存在一個age=27, id=15的記錄了,此時再來一個age=27, id=14的記錄,那這個id=14的記錄必須排在id=15的前面。而此時id=14的位置是被間隙鎖鎖住的。所以它是插入失敗的。
  • 上面凡是紅色的背景或紅色箭頭的區域都是有鎖的區域,也就是這樣的數據不能被插入成功。

實驗前的預測:

 


實驗截圖:

 


實驗使用到的SQL語句:

insert into userinfo (id, name, age, phone, remark) values(1, 'name1', 20, 'phone1', 'id=1導致被阻塞,改爲id<1的值就可以成功');
insert into userinfo (id, name, age, phone, remark) values(-1, 'name-1', 21, 'phone-1', 'age=21導致被阻塞,改爲age<21或age>27就可以成功');
insert into userinfo (id, name, age, phone, remark) values(1, 'name1', 21, 'phone1', 'age=21導致被阻塞,只要20<age<27,不管其他列是什麼值,都會被阻塞');
insert into userinfo (id, name, age, phone, remark) values(-1, 'name-1', 20, 'phone-1', '可以插入成功');
insert into userinfo (id, name, age, phone, remark) values(14, 'name14', 27, 'phone14', 'id=14導致被阻塞,改爲id>15就可以插入成功');
insert into userinfo (id, name, age, phone, remark) values(16, 'name16', 26, 'phone16', 'age=26導致被阻塞,改爲age<20或age>26就可以成功');
insert into userinfo (id, name, age, phone, remark) values(14, 'name14', 26, 'phone14', 'age=26導致被阻塞,只要20<age<27,不管其他列是什麼值,都會被阻塞');
insert into userinfo (id, name, age, phone, remark) values(16, 'name16', 27, 'phone16', '可以插入成功');

 

RR級別+普通字段列
如果是使用如下的一個普通字段來加鎖,會是什麼情況呢?

select * from userinfo where name = 'liuqiangdong' for update;

 

此時加鎖的情況是這樣的:因爲name是一個普通的列,上面沒有任何索引可以使用,所以根據這個條件進行搜索數據的時候,會進行全表掃描的操作,當搜索到第一個name='liuqiangdong’的數據行的時候,不會馬上停下來,因爲後面可能還有很多個name='liuqiangdong’的數據行,所以要進行全表掃描。根據前面我們說的加鎖規則,凡是掃描到的對象都要加鎖,所以此時全表中的每一個主鍵索引記錄上都有一個X鎖。同時爲了防止幻讀的凡是,會對這個表上所有的主鍵索引的間隙也都增加上間隙鎖。

加鎖情況示意圖如下:

 

實驗過程:

 


總結
在可重複讀RR隔離級別下,間隙鎖的加鎖規則如下,如果不是RR級別,也就不會有間隙鎖存在了,所以間隙鎖只在RR級別下才會存在。

  • 原則 1:加鎖的基本單位是臨鍵鎖next-key lock。next-key lock是前開後閉區間。
  • 原則 2:查找過程中訪問到的對象纔會加鎖。
  • 優化 1:索引上的等值查詢,給唯一索引加鎖的時候,next-key lock 退化爲行鎖。
  • 優化 2:索引上的等值查詢,向右遍歷時且最後一個值不滿足等值條件的時候,next-key lock 退化爲間隙鎖。
  • 一個 bug:唯一索引上的範圍查詢會訪問到不滿足條件的第一個值爲止,這個在MySQL8.0以後已經修復,但是在MySQL5.7版本中有這個問題。

————————————————

版權聲明:本文爲博主原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接和本聲明。

原文鏈接:https://blog.csdn.net/javaanddonet/article/details/111187345

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章