MySQL InnoDB 可重複讀下的排他鎖探索
排他鎖是一種獨佔鎖,用於獨佔資源。以多人輪流使用吹風機吹頭髮為例子。獨佔什麼資源?獨佔吹風機,在獨佔的期間內,只有這個人能使用吹風機,獨佔結束後,別人嘗試獨佔吹風機。當然之前用過吹風機的人也可以繼續嘗試獨佔。如何嘗試獨佔?眾人可以擲骰子,可以排隊等等。鎖在哪裡?在這裡,鎖是隱式的,看不到的,存在於「吹風機同一時刻只能服務一個人」這句話裡。
本文主要通過一些示例,觀察SQL/">MySQL InnoDB中,可重複讀隔離級別下,行鎖中排他鎖的表現。
注意,對於排他鎖
有三個限定修飾:
- InnoDB 儲存引擎
- 事務隔離級別是可重複讀。這也是MySQL預設的事務隔離級別。
- 行鎖
再加一個限定:MySQL 5.6版本,預設配置 —— 本文中除非特別說明,都是這個配置。
基礎知識
先準備一些基礎知識。這些整理自網路和《MySQL技術內幕-InnoDB儲存引擎》。
事務的4個特性:ACID
特性 | 英文 | 說明 |
---|---|---|
原子性 | Atomicity | 一個事物內所有操作共同組成一個原子包,要麼全部成功,要麼全部失敗 |
一致性 | Consistency | 事務將資料庫從一種狀態轉變為下一種一致性狀態。在事務開始之前和結束之後,事務的完整性約束沒有被破壞。 |
隔離性 | Isolation | 事務之間不干擾。 |
永續性 | Durability | 事務一旦提交,其結果是永久性的。即使發生當即等故障,資料庫也能叫資料恢復。 |
對於一致性的一種解釋是,事務開始前和結束後,完整性約束沒有被破壞,比如某一列要求每個資料具有唯一性,那麼事務前後,無論有無新增資料,這一列的每列資料仍然具有唯一性。
另一種關於一致性的解釋中更關注中間狀態。一致性要求中間狀態不被其他事務感知。但出於效能等方面考慮,不同的隔離性
程度通過對一致性
不同程度的破壞來提升效能和併發能力。
隔離性的程度,就是隔離級別。
隔離級別
隔離級別 | 是否會髒讀 | 是否會不可重複讀 | 是否會幻讀 |
---|---|---|---|
讀未提交 | 是 | 是 | 是 |
讀已提交 | 否 | 是 | 是 |
可重複讀 | 否 | 否 | 是 |
序列化 | 否 | 否 | 否 |
上表中,隔離級別依次增高。隔離級別越高,越不會出現奇怪的*讀
問題。下表示關於髒讀
、不可重複讀
、幻讀
的解釋:
概念 | 解釋 |
---|---|
髒讀 | 當一個事務正在訪問資料,並且對資料進行了修改,而這種修改還沒有提交到資料庫中,這時,另外一個事務也訪問這個資料,然後使用了這個資料 |
不可重複讀 | 在一個事務內,多次讀同一資料。在這個事務還沒有結束時,另外一個事務也訪問該同一資料。那麼,在第一個事務中的兩次讀資料之間,由於第二個事務的修改,那麼第一個事務兩次讀到的的資料可能是不一樣的。這樣在一個事務內兩次讀到的資料是不一樣的,因此稱為是不可重複讀。 |
幻讀 | 例如第一個事務對一個表中的資料進行了修改,這種修改涉及到表中的全部資料行。同時,第二個事務也修改這個表中的資料,這種修改是向表中插入一行新資料。那麼,以後就會發生操作第一個事務的使用者發現表中還有沒有修改的資料行,就好象發生了幻覺一樣。 |
一般的說法是隔離級別越高,效能越差。首先序列化的效能的確很差。事務中一個純粹的一個select操作就會把資料鎖住。但是我看到網上一些對比,可重複讀的效能並不比讀已提交差。另外,MySQL的可重複讀,從某個角度而言,沒有幻讀問題。
索引的底層實現
索引用來做什麼?用於快速檢索資料。
InnoDB的索引基於B+樹。行鎖是基於索引的。B+樹是一種平衡查詢樹,在基於磁頭的硬碟中查詢效能很高。
在B+樹中,非葉子節點充當的是索引的作用。所有的葉子結點中包含了全部關鍵字的資訊,及指向含這些關鍵字記錄的指標,且葉子結點本身依關鍵字的大小自小而大順序連結。
在 MySQL 中,索引有聚集索引和輔助索引之分:
名詞 | 解釋 |
---|---|
聚集索引 | 按照一張表的主鍵構造一顆B+樹,葉子節點存放的是整張表的行記錄資料。 |
輔助索引 | 也叫非聚集索引。葉節點除了包含鍵值以外,每個葉級別中的索引行中還包含了一個書籤(bookmark),該書籤就是相應行資料的聚集索引鍵。 |
如果一張表在建立時沒設定主鍵怎麼辦?沒關係,MySQL會幫忙建立一個我們看不到的主鍵。
另外,索引也可以劃分為「唯一索引」和「非唯一索引」。
鎖之間的相容性
InnoDB中有很多種鎖,比如針對表這個粒度有共享鎖(S)、排他鎖(X)、意向共享鎖(IS)、意向排他鎖(IX)。表級相容性如下:
X | IX | S | IS | |
---|---|---|---|---|
X | Conflict | Conflict | Conflict | Conflict |
IX | Conflict | Compatible | Conflict | Compatible |
S | Conflict | Conflict | Compatible | Compatible |
IS | Conflict | Compatible | Compatible | Compatible |
什麼時候用到意向鎖?在鎖行之前,會對錶加上對應的意向鎖。
注意,意向鎖之間是互相相容的,但與共享鎖、排他鎖不全部相容。那麼要意向鎖幹嘛用?
想象下,當前表中有10條資料被加上了行鎖,現在要對錶加排他鎖(X),肯定要判斷是不是有資料已經被鎖了,如果有,那對錶加排他鎖的操作就要等一下,等到表中沒有任何鎖為止。
那麼如何判斷是不是有資料已經被鎖?兩種方案:
- 一條條資料掃描
- 根據意向鎖
可以看出,意向鎖是最合適的方案。
行鎖分為共享鎖和排他鎖。
再看下行鎖之間的相容性:
X | S | |
---|---|---|
X | Conflict | Conflict |
S | Conflict | Compatible |
有哪些會導致行鎖呢?
select * lock in share mode select * for update udpate delete insert
如果要使用行鎖鎖住的資料太多,會升級為表鎖。什麼叫太多?一個測試結果是超過20%的資料。注意,20%只是一個參考值,並非確定值。
行鎖的另一種劃分,是分成記錄鎖、間隙鎖、next-key鎖等。含義不表,均在下面的示例中提現。
什麼時候釋放鎖呢?
- 如果是單條SQL,執行完後釋放鎖。
- 如果在事務中鎖資料,等事務結束後,鎖被釋放。
提問:
MySQL的鎖表指令是什麼?
探索(一)
資料準備
建立資料庫和table:
CREATE DATABASE `test01`; CREATE DATABASE `test02`; CREATE TABLE `test01`.`ttt` ( id BIGINT(20) PRIMARY KEY NOT NULL AUTO_INCREMENT, num BIGINT(20) DEFAULT 0 NOT NULL )ENGINE =InnoDB DEFAULT CHARSET =utf8mb4; CREATE TABLE `test02`.`ttt` ( id BIGINT(20) PRIMARY KEY NOT NULL AUTO_INCREMENT, num BIGINT(20) DEFAULT 0 NOT NULL )ENGINE =InnoDB DEFAULT CHARSET =utf8mb4;
插入資料:
INSERT INTO test01.ttt(id, num) VALUES(1, 1001); INSERT INTO test01.ttt(id, num) VALUES(2, 1002); INSERT INTO test02.ttt(id, num) VALUES(1, 2001); INSERT INTO test02.ttt(id, num) VALUES(2, 2002);
檢視資料:
mysql> select * from test01.ttt; +----+------+ | id | num| +----+------+ |1 | 1001 | |2 | 1002 | +----+------+ mysql> select * from test02.ttt; +----+------+ | id | num| +----+------+ |1 | 2001 | |2 | 2002 | +----+------+
示例1
下面的會話
是指,開啟終端,輸入mysql -uroot -p
,然後輸密碼,進入與MySQL伺服器的互動。若有多個會話,則是打開了多個終端。。
步驟 | 會話 |
---|---|
1 | start transaction; |
2 | INSERT INTO test01.ttt(id, num) VALUES(3, 1003); |
3 | INSERT INTO test02.ttt(id, num) VALUES(3, 2003); |
4 | rollback; |
5 | select * from test01.ttt; |
6 | select * from test02.ttt; |
因為第4會回滾了,第5、6步查到的資料都還是原先的兩條。這裡驗證了,一個事務中可以操作一個MySQL例項中多個數據庫中的資料。
示例2
驗證鎖的可重入。
步驟 | 會話 |
---|---|
1 | start transaction; |
2 | select * from test01.ttt where id=1 for update; |
3 | select * from test01.ttt where id=1 or id=2 for update; |
4 | select * from test01.ttt where id=1 for update; |
步驟3:
+----+------+ | id | num| +----+------+ |1 | 1001 | +----+------+
步驟4:
+----+------+ | id | num| +----+------+ |1 | 1001 | |2 | 1002 | +----+------+
步驟5:
+----+------+ | id | num| +----+------+ |1 | 1001 | +----+------+
示例3
步驟 | 會話1 | 會話2 |
---|---|---|
1 | start transaction; | start transaction; |
2 | select * from test01.ttt where id = 1 for update; | |
3 | select * from test02.ttt where id = 1 for update; | |
4 | select * from test01.ttt where id = 1 for update; | |
5 | select * from test02.ttt where id = 1 for update; | |
6 | select * from test01.ttt; | |
7 | updatetest01.ttt set num=123 where id=1; |
會話1:
第2、3步驟,直接輸出資料。會對test01.ttt
、test02.ttt
中id為1的資料加排他鎖。
會話2:
第4步會發生什麼?
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
事務還在。
第5步將發生什麼?
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
第6步將發生什麼?
直接取出資料。
第7步將發生什麼?
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
出現了鎖等待超時錯誤。這個等待時間,預設是50秒。可以通過select @@innodb_lock_wait_timeout;
檢視。
注意,出現這種錯誤時,會話2的事務並沒有結束。怎麼驗證?可以通過執行下面的SQL檢視當前有哪些事務:
SELECT * FROM information_schema.INNODB_TRX \G
示例4
步驟 | 會話1 | 會話2 |
---|---|---|
1 | start transaction; | |
2 | select * from test01.ttt where id = 1 for update; | |
3 | select * from test01.ttt where id = 1 for update; | |
4 | select * from test01.ttt where id = 2 for update; |
會話1:
步驟2將直接取出資料:
+----+------+ | id | num| +----+------+ |1 | 1001 | +----+------+
會話2:
步驟3將:
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
步驟4將:直接輸出結果
+----+------+ | id | num| +----+------+ |2 | 1002 | +----+------+
會話2雖然沒有看起事務,但步驟3依然出現鎖超時。可以認為一個單條的SQL可以看做一個單獨的事務。
示例5
步驟 | 會話1 | 會話2 |
---|---|---|
1 | start transaction; | start transaction; |
2 | INSERT INTO test01.ttt(id, num) VALUES(3, 1003); | |
3 | INSERT INTO test01.ttt(id, num) VALUES(3, 1003); |
會話1:
步驟2:
Query OK, 1 row affected (0.00 sec)
會話2:
步驟3:
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
是的,id為3,被會話1鎖了。
示例6
步驟 | 會話1 | 會話2 |
---|---|---|
1 | start transaction; | start transaction; |
2 | updatetest01.ttt set num=999 where id=1; | |
3 | select * from test01.ttt where id = 1; | |
4 | select * from test01.ttt where id = 1 for update; | |
5 | select * from test01.ttt where id = 1; | |
6 | rollback; | |
7 | select * from test01.ttt where id = 1; |
會話1:
步驟2會鎖資料。
步驟3:
+----+-----+ | id | num | +----+-----+ |1 | 999 | +----+-----+
步驟7:
+----+------+ | id | num| +----+------+ |1 | 1001 | +----+------+
會話2:
步驟4:
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
步驟5:
+----+------+ | id | num| +----+------+ |1 | 1001 | +----+------+
示例7
步驟 | 會話1 | 會話2 |
---|---|---|
1 | start transaction; | start transaction; |
2 | select * from test01.ttt where id = 3 for update; | |
3 | select * from test01.ttt where id = 3 for update; | |
4 | INSERT INTO test01.ttt(id, num) VALUES(3, 1003); | |
5 | INSERT INTO test01.ttt(id, num) VALUES(4, 1004); |
會話1:
步驟2:
Empty set (0.00 sec)
沒有資料,但這裡其實把(2,∞)這個範圍的id都鎖住了。這就是間隙鎖。
會話2:
步驟3:
Empty set (0.00 sec)
雖然id=3被會話1鎖了,但因為沒資料,這一步直接給了結果。
步驟4、5:
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
示例8
步驟 | 會話1 | 會話2 |
---|---|---|
1 | start transaction; | |
2 | select * from test01.ttt where id >0 and id<10 for update; | |
3 | select * from test01.ttt where id = 3 for update; | |
4 | INSERT INTO test01.ttt(id, num) VALUES(3, 1003); | |
5 | select * from test01.ttt where id = 2 for update; |
會話1:
步驟2:
+----+------+ | id | num| +----+------+ |1 | 1001 | |2 | 1002 | +----+------+
得到兩條資料,不過(0, 10)
範圍的id都被鎖了。
會話2:
步驟3:
Empty set (0.00 sec)
步驟4:
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
步驟5:
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
示例9
步驟 | 會話1 | 會話2 |
---|---|---|
1 | start transaction; | |
2 | select * from test01.ttt where id >2 for update; | |
3 | select * from test01.ttt where id = 3 for update; | |
4 | select * from test01.ttt where id = 2 for update; | |
5 | INSERT INTO test01.ttt(id, num) VALUES(3, 1003); |
會話1:
步驟2:
Empty set (0.00 sec)
(2, ∞)
範圍的id都被鎖了。
會話2:
步驟3:
Empty set (0.00 sec)
步驟4:
+----+------+ | id | num| +----+------+ |2 | 1002 | +----+------+
步驟5:
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
示例10
步驟 | 會話1 | 會話2 |
---|---|---|
1 | start transaction; | |
2 | select * from test01.ttt where id = 3 for update; | |
3 | select * from test01.ttt where id = 3 for update; | |
4 | INSERT INTO test01.ttt(id, num) VALUES(3, 1003); | |
5 | select * from test01.ttt where id = 2 for update; |
會話1:
步驟2:
Empty set (0.00 sec)
會話2:
步驟3:
Empty set (0.00 sec)
步驟4:
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
步驟5:
+----+------+ | id | num| +----+------+ |2 | 1002 | +----+------+
示例11
步驟 | 會話1 | 會話2 |
---|---|---|
1 | start transaction; | start transaction; |
2 | updatetest01.ttt set num=999 where id=1; | |
3 | updatetest01.ttt set num=9999 where id=1; |
會話1:
步驟2:
Query OK, 1 row affected (0.03 sec) Rows matched: 1Changed: 1Warnings: 0
會話2:
步驟3:
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
示例12
步驟 | 會話1 | 會話2 |
---|---|---|
1 | start transaction; | |
2 | select * from test01.ttt where num=1001 for update; | |
3 | select * from test01.ttt where id = 2 for update; |
會話1:
步驟2:
+----+------+ | id | num| +----+------+ |1 | 1001 | +----+------+
因為where中沒用索引進行查詢,所以,鎖表了。
會話2:
步驟3:
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
示例13
驗證可重複讀。
步驟 | 會話1 | 會話2 |
---|---|---|
1 | start transaction; | |
2 | select * from test01.ttt; | |
3 | updatetest01.ttt set num=9999 where id=1; | |
4 | select * from test01.ttt; | |
5 | select * from test01.ttt; | |
6 | select * from test01.ttt where id=1 for update; | |
7 | select * from test01.ttt; |
會話1:
步驟2:
+----+------+ | id | num| +----+------+ |1 | 1001 | |2 | 1002 | +----+------+
步驟4、7:
+----+------+ | id | num| +----+------+ |1 | 1001 | |2 | 1002 | +----+------+
步驟5:
+----+------+ | id | num| +----+------+ |1 | 9999 | +----+------+
select是快照讀,讀的資料是select在該事務中執行那一刻之前,資料庫中已經提交的資料。除非本事務對資料有修改,否則,多次同樣select的結果是一樣的。對應的,select for update叫做當前讀。
會話2:
步驟3:
Query OK, 1 row affected (0.01 sec) Rows matched: 1Changed: 1Warnings: 0
步驟5:
+----+------+ | id | num| +----+------+ |1 | 9999 | |2 | 1002 | +----+------+
示例14
驗證可重複讀。
步驟 | 會話1 | 會話2 |
---|---|---|
1 | select * from test01.ttt; | |
2 | start transaction; | |
3 | updatetest01.ttt set num=9999 where id=1; | |
4 | select * from test01.ttt; | |
5 | select * from test01.ttt; |
會話1:
步驟1:
+----+------+ | id | num| +----+------+ |1 | 1001 | |2 | 1002 | +----+------+
步驟4:
+----+------+ | id | num| +----+------+ |1 | 9999 | |2 | 1002 | +----+------+
會話2:
步驟5:
+----+------+ | id | num| +----+------+ |1 | 9999 | |2 | 1002 | +----+------+
示例15
步驟 | 會話1 | 會話2 |
---|---|---|
1 | start transaction; | |
2 | select * from test01.ttt; | |
3 | updatetest01.ttt set num=9999 where id=1; | |
4 | select * from test01.ttt; | select * from test01.ttt; |
5 | commit; | |
6 | select * from test01.ttt; |
會話1:
步驟3:
Query OK, 1 row affected (0.00 sec) Rows matched: 1Changed: 1Warnings: 0
步驟4:
+----+------+ | id | num| +----+------+ |1 | 9999 | |2 | 1002 | +----+------+
會話2:
步驟2:
+----+------+ | id | num| +----+------+ |1 | 1001 | |2 | 1002 | +----+------+
步驟4:
+----+------+ | id | num| +----+------+ |1 | 1001 | |2 | 1002 | +----+------+
步驟6:
+----+------+ | id | num| +----+------+ |1 | 9999 | |2 | 1002 | +----+------+
示例16
驗證快照讀不會出現幻讀。
步驟 | 會話1 | 會話2 |
---|---|---|
1 | start transaction; | |
2 | select * from test01.ttt; | |
3 | INSERT INTO test01.ttt(id, num) VALUES(3, 1003); | |
4 | select * from test01.ttt; | |
5 | select * from test01.ttt; |
會話1:
步驟2:
+----+------+ | id | num| +----+------+ |1 | 1001 | |2 | 1002 | +----+------+
步驟4:
+----+------+ | id | num| +----+------+ |1 | 1001 | |2 | 1002 | +----+------+
會話2:
步驟5:
+----+------+ | id | num| +----+------+ |1 | 1001 | |2 | 1002 | |3 | 1003 | +----+------+
示例17
驗證快照讀不會出現幻讀。
步驟 | 會話1 | 會話2 |
---|---|---|
1 | start transaction; | |
2 | select * from test01.ttt where id=3; | |
3 | INSERT INTO test01.ttt(id, num) VALUES(3, 1003); | |
4 | select * from test01.ttt where id=3; | |
5 | select * from test01.ttt; |
會話1:
步驟2、4,無資料:
Empty set (0.00 sec)
會話2:
步驟5:
+----+------+ | id | num| +----+------+ |1 | 1001 | |2 | 1002 | |3 | 1003 | +----+------+
示例18
驗證快照讀不會出現幻讀。
步驟 | 會話1 | 會話2 |
---|---|---|
1 | start transaction; | |
2 | select * from test01.ttt; | |
3 | delete from test01.ttt where id=2; | |
4 | select * from test01.ttt; | |
5 | select * from test01.ttt; |
會話1:
步驟2:
+----+------+ | id | num| +----+------+ |1 | 1001 | |2 | 1002 | +----+------+
步驟4:
+----+------+ | id | num| +----+------+ |1 | 1001 | |2 | 1002 | +----+------+
會話2:
步驟3:
Query OK, 1 row affected (0.00 sec)
步驟5:
+----+------+ | id | num| +----+------+ |1 | 1001 | +----+------+
示例19
出現幻讀。
步驟 | 會話1 | 會話2 |
---|---|---|
1 | start transaction; | |
2 | select * from test01.ttt where id=3; | |
3 | INSERT INTO test01.ttt(id, num) VALUES(3, 1003); | |
4 | INSERT INTO test01.ttt(id, num) VALUES(3, 1003); | |
5 | select * from test01.ttt; | |
6 | select * from test01.ttt where id=3 for update; |
會話1:
步驟2,無資料:
Empty set (0.00 sec)
步驟4:
ERROR 1062 (23000): Duplicate entry '3' for key 'PRIMARY'
步驟4會比較困惑,「之前不是沒資料嗎,怎麼又有資料了:cry:」。解決辦法很簡單,將步驟2改成select for update。
會話2:
步驟5:
+----+------+ | id | num| +----+------+ |1 | 1001 | |2 | 1002 | |3 | 1003 | +----+------+
步驟6:
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
示例20
步驟 | 會話1 | 會話2 |
---|---|---|
1 | start transaction; | |
2 | select * from test01.ttt; | |
3 | UPDATE test01.ttt set num=num+1 where id=2; | |
4 | select * from test01.ttt; | select * from test01.ttt; |
5 | UPDATE test01.ttt set num=num+1000 where id=2; | |
6 | commit; | |
7 | select * from test01.ttt; |
會話1:
步驟2:
+----+------+ | id | num| +----+------+ |1 | 1001 | |2 | 1002 | +----+------+
步驟4:
+----+------+ | id | num| +----+------+ |1 | 1001 | |2 | 1002 | +----+------+
步驟5:
Query OK, 1 row affected (0.01 sec) Rows matched: 1Changed: 1Warnings: 0
會話2:
步驟3:
Query OK, 1 row affected (0.01 sec) Rows matched: 1Changed: 1Warnings: 0
步驟4:
+----+------+ | id | num| +----+------+ |1 | 1001 | |2 | 1003 | +----+------+
步驟6:
+----+------+ | id | num| +----+------+ |1 | 1001 | |2 | 1003 | +----+------+
步驟8:
+----+------+ | id | num| +----+------+ |1 | 1001 | |2 | 2003 | +----+------+
示例21
步驟 | 會話1 | 會話2 |
---|---|---|
1 | start transaction; | |
2 | select * from test01.ttt; | |
3 | UPDATE test01.ttt set num=num+1 where id=2; | |
4 | select * from test01.ttt; | select * from test01.ttt; |
5 | UPDATE test01.ttt set num=num+1000 where id=2; | |
6 | UPDATE test01.ttt set num=num+2 where id=2; | |
7 | commit; | |
8 | select * from test01.ttt; |
會話2步驟6,鎖超時。
示例22
死鎖示例。
步驟 | 會話1 | 會話2 |
---|---|---|
1 | start transaction; | start transaction; |
2 | select * from test01.ttt where id=1 for update; | |
3 | select * from test01.ttt where id=2 for update; | |
4 | select * from test01.ttt where id=2 for update; | |
5 | select * from test01.ttt where id=1 for update; |
會話1:
步驟2:
+----+------+ | id | num| +----+------+ |1 | 1001 | +----+------+
步驟4:
先是等待。然後會話2步驟5執行後,輸出:
+----+------+ | id | num| +----+------+ |2 | 1002 | +----+------+
會話2:
步驟3:
+----+------+ | id | num| +----+------+ |2 | 1002 | +----+------+
步驟5:
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
死鎖檢測出後,會話2事務會結束。
探索(二)
資料準備
CREATE TABLE `test01`.`sss` ( `id` BIGINT(20) PRIMARY KEY NOT NULL AUTO_INCREMENT, `num` BIGINT(20) DEFAULT 0 NOT NULL, `age` BIGINT(20) DEFAULT 0 NOT NULL, KEY `idx_num` (`num`) )ENGINE =InnoDB DEFAULT CHARSET =utf8mb4; INSERT INTO test01.sss(id, num, age) VALUES(1, 3001, 20); INSERT INTO test01.sss(id, num, age) VALUES(2, 3002, 21); INSERT INTO test01.sss(id, num, age) VALUES(3, 3002, 22); INSERT INTO test01.sss(id, num, age) VALUES(10, 4000, 23); mysql> select * from test01.sss; +----+------+-----+ | id | num| age | +----+------+-----+ |1 | 3001 |20 | |2 | 3002 |21 | |3 | 3002 |22 | | 10 | 4000 |23 | +----+------+-----+
表sss
中id是自增主鍵,num列增加了非唯一索引。
示例1
步驟 | 會話1 | 會話2 |
---|---|---|
1 | start transaction; | |
2 | select * from test01.sss where id=3for update; | |
3 | select * from test01.sss where num=3002 and age=22 for update; | |
4 | select * from test01.sss where num=3003 for update; | |
5 | INSERT INTO test01.sss(id, num, age) VALUES(4, 3003, 22); | |
6 | INSERT INTO test01.sss(id, num, age) VALUES(5, 3002, 22); |
會話1:
步驟2:
+----+------+-----+ | id | num| age | +----+------+-----+ |3 | 3002 |22 | +----+------+-----+
會話2:
步驟3:
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
基於num查詢資料,先從num對應的輔助索引查詢id是3,然後去聚集索引找id為3的資料內容。但這條資料被鎖上了。所以,所等待超時。
步驟4:
Empty set (0.00 sec)
步驟5:
Query OK, 1 row affected (0.01 sec)
步驟6:
Query OK, 1 row affected (0.01 sec)
示例2
驗證鎖行是基於索引的。
步驟 | 會話1 | 會話2 |
---|---|---|
1 | start transaction; | |
2 | select * from test01.sss where num=3002 and age=21 for update; | |
3 | select * from test01.sss where num=3002 and age=22 for update; | |
4 | select * from test01.sss where num=3003 for update; | |
5 | INSERT INTO test01.sss(id, num, age) VALUES(4, 3003, 22); | |
6 | select * from test01.sss where num=4000 for update; |
會話1:
步驟2:
+----+------+-----+ | id | num| age | +----+------+-----+ |2 | 3002 |21 | +----+------+-----+
有兩條資料的num都是3002,其中一條的age是21。num上有非唯一索引
,這裡鎖資料會鎖住3002
、(3001, 3002)
、(3002, 4000)
這三個位置/範圍。防止其他事務插入資料,導致新插入num為3002的資料,從而防止幻讀。
為什麼這麼鎖?想想B+樹,想想非唯一索引。這種鎖能解決幻讀問題。
可不可以只鎖3002?肯定有技術方案可以做到,但是MySQL沒選那個方案。
會話2:
步驟3:
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
因為被會話1鎖了,所以這裡鎖超時。
步驟4:
Empty set (0.00 sec)
步驟5:
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
步驟6:
+----+------+-----+ | id | num| age | +----+------+-----+ | 10 | 4000 |23 | +----+------+-----+
示例3
步驟 | 會話1 | 會話2 |
---|---|---|
1 | start transaction; | |
2 | select * from test01.sss; | |
3 | select * from test01.sss where id =10for update; | |
4 | updatetest01.sss set age=30 where num=4000; |
會話1:
步驟2:
+----+------+-----+ | id | num| age | +----+------+-----+ |1 | 3001 |20 | |2 | 3002 |21 | |3 | 3002 |22 | | 10 | 4000 |23 | +----+------+-----+
步驟3:
+----+------+-----+ | id | num| age | +----+------+-----+ | 10 | 4000 |23 | +----+------+-----+
會話2:
步驟4:
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
示例4
步驟 | 會話1 | 會話2 |
---|---|---|
1 | INSERT INTO test01.sss(id, num, age) VALUES(16, 4800, 23); | |
2 | start transaction; | |
3 | select * from test01.sss; | |
4 | select * from test01.sss where num = 4300 for update; | |
5 | INSERT INTO test01.sss(id, num, age) VALUES(22, 4300, 22); | |
6 | INSERT INTO test01.sss(id, num, age) VALUES(20, 4200, 22); | |
7 | INSERT INTO test01.sss(id, num, age) VALUES(21, 4500, 22); | |
8 | INSERT INTO test01.sss(id, num, age) VALUES(22, 5000, 22); |
會話1:
步驟3:
+----+------+-----+ | id | num| age | +----+------+-----+ |1 | 3001 |20 | |2 | 3002 |21 | |3 | 3002 |22 | | 10 | 4000 |23 | | 16 | 4800 |23 | +----+------+-----+
步驟4:
Empty set (0.00 sec)
會話2:
步驟5、6、7:
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
步驟6、7:
Query OK, 1 row affected (0.01 sec)
示例5
步驟 | 會話1 | 會話2 |
---|---|---|
1 | INSERT INTO test01.sss(id, num, age) VALUES(16, 4800, 23); | |
2 | start transaction; | |
3 | select * from test01.sss; | |
4 | select * from test01.sss where num = 4800 for update; | |
5 | INSERT INTO test01.sss(id, num, age) VALUES(20, 4200, 22); | |
6 | INSERT INTO test01.sss(id, num, age) VALUES(21, 5000, 22); | |
7 | INSERT INTO test01.sss(id, num, age) VALUES(22, 4800, 22); |
會話1:
步驟3:
+----+------+-----+ | id | num| age | +----+------+-----+ |1 | 3001 |20 | |2 | 3002 |21 | |3 | 3002 |22 | | 10 | 4000 |23 | | 16 | 4800 |23 | +----+------+-----+
步驟4:
+----+------+-----+ | id | num| age | +----+------+-----+ | 16 | 4800 |23 | +----+------+-----+
會話2:
步驟5、6、7:
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
示例6
步驟 | 會話1 | 會話2 |
---|---|---|
1 | start transaction; | |
2 | select * from test01.sss; | |
3 | INSERT INTO test01.sss(id, num, age) VALUES(16, 4800, 23); | |
4 | select * from test01.sss | |
5 | INSERT INTO test01.sss(id, num, age) VALUES(20, 4200, 22); | |
6 | INSERT INTO test01.sss(id, num, age) VALUES(21, 5000, 22); | |
7 | INSERT INTO test01.sss(id, num, age) VALUES(22, 4800, 22); |
會話1:
步驟2:
+----+------+-----+ | id | num| age | +----+------+-----+ |1 | 3001 |20 | |2 | 3002 |21 | |3 | 3002 |22 | | 10 | 4000 |23 | +----+------+-----+
步驟4:
+----+------+-----+ | id | num| age | +----+------+-----+ |1 | 3001 |20 | |2 | 3002 |21 | |3 | 3002 |22 | | 10 | 4000 |23 | | 16 | 4800 |23 | +----+------+-----+
會話2:
步驟5、6、7:
Query OK, 1 row affected (0.01 sec)
看起來insert只對主鍵上鎖。或者是對唯一索引上鎖。
探索(三)
資料準備
CREATE TABLE `test01`.`kkk` ( `id` BIGINT(20) PRIMARY KEY NOT NULL AUTO_INCREMENT, `num` BIGINT(20) DEFAULT 0 NOT NULL, `age` BIGINT(20) DEFAULT 0 NOT NULL, UNIQUE INDEX `uk_num` (`num`), KEY `idx_age` (`age`) )ENGINE =InnoDB DEFAULT CHARSET =utf8mb4; INSERT INTO test01.kkk(id, num, age) VALUES(1, 3001, 20); INSERT INTO test01.kkk(id, num, age) VALUES(2, 3002, 21); INSERT INTO test01.kkk(id, num, age) VALUES(3, 3003, 22); INSERT INTO test01.kkk(id, num, age) VALUES(10, 4000, 23);
num具有了唯一索引。age具有非唯一索引。
示例1
步驟 | 會話1 | 會話2 |
---|---|---|
1 | start transaction; | |
2 | select * from test01.kkk; | |
3 | INSERT INTO test01.kkk(id, num, age) VALUES(16, 4800, 23); | |
4 | INSERT INTO test01.kkk(id, num, age) VALUES(16, 4801, 24); | |
5 | INSERT INTO test01.kkk(id, num, age) VALUES(17, 4800, 25); | |
6 | INSERT INTO test01.kkk(id, num, age) VALUES(18,4803, 23); | |
7 | INSERT INTO test01.sss(id, num, age) VALUES(19, 4804, 27); |
會話1
步驟2:
+----+------+-----+ | id | num| age | +----+------+-----+ |1 | 3001 |20 | |2 | 3002 |21 | |3 | 3003 |22 | | 10 | 4000 |23 | +----+------+-----+
步驟3:
Query OK, 1 row affected (0.00 sec)
會話2:
步驟4:
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
步驟5:
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
步驟6:
Query OK, 1 row affected (0.01 sec)
步驟7:
Query OK, 1 row affected (0.01 sec)
id和num都是唯一索引,所以id和num是一一對應的關係。插入資料時,對id、num加了鎖,所以會話2步驟4、5鎖超時 。