InnoDB的鎖機制淺析

来源:https://www.cnblogs.com/AaronCui/archive/2019/03/09/10490663.html
-Advertisement-
Play Games

InnoDB的鎖機制淺析 1. 前言 數據事務設計遵循ACID的原則。 MySQL資料庫提供了四種預設的隔離級別,讀未提交( read uncommitted )、讀已提交(或不可重覆讀)( read committed )、可重覆讀( repeatable read )、串列化( serializ ...


InnoDB的鎖機制淺析

本文是大而全版(五合一):InnoDB的鎖機制淺析(All in One)

如果想分開看可以點擊下麵鏈接:

文章總共分為五個部分:

1. 前言

數據事務設計遵循ACID的原則。

MySQL資料庫提供了四種預設的隔離級別,讀未提交(read-uncommitted)、讀已提交(或不可重覆讀)(read-committed)、可重覆讀(repeatable-read)、串列化(serializable)。

MySQL的預設隔離級別是RR。

2. 鎖基本概念

2.1 共用鎖和排它鎖

InnoDB實現了兩種標準行級鎖,一種是共用鎖(shared locks,S鎖),另一種是獨占鎖,或者叫排它鎖(exclusive locks,X鎖)。

S鎖允許當前持有該鎖的事務讀取行。
X鎖允許當前持有該鎖的事務更新或刪除行。

S鎖

如果事務T1持有了行r上的S鎖,則其他事務可以同時持有行r的S鎖,但是不能對行r加X鎖

X鎖

如果事務T1持有了行r上的X鎖,則其他任何事務不能持有行r的X鎖,必須等待T1在行r上的X鎖釋放。

如果事務T1在行r上保持S鎖,則另一個事務T2對行r的鎖的請求按如下方式處理:

  • T2可以同時持有S鎖
  • T2如果想在行r上獲取X鎖,必須等待其他事務對該行添加的S鎖X鎖的釋放。

2.2 意向鎖-Intention Locks

InnoDB支持多種粒度的鎖,允許行級鎖和表級鎖的共存。例如LOCK TABLES ... WRITE等語句可以在指定的表上加上獨占鎖。
InnoBD使用意向鎖來實現多個粒度級別的鎖定。意向鎖是表級鎖,表示table中的row所需要的鎖(S鎖或X鎖)的類型。

意向鎖分為意向共用鎖(IS鎖)和意向排它鎖(IX鎖)。
IS鎖表示當前事務意圖在表中的行上設置共用鎖,下麵語句執行時會首先獲取IS鎖,因為這個操作在獲取S鎖:

SELECT ... LOCK IN SHARE MODE

IX鎖表示當前事務意圖在表中的行上設置排它鎖。下麵語句執行時會首先獲取IX鎖,因為這個操作在獲取X鎖:

SELECT ... FOR UPDATE

事務要獲取某個表上的S鎖和X鎖之前,必須先分別獲取對應的IS鎖和IX鎖。

2.3 鎖的相容性

鎖的相容矩陣如下:

--- 排它鎖(X) 意向排它鎖(IX) 共用鎖(S) 意向共用鎖(IS)
排它鎖(X) N N N N
意向排它鎖(IX) N OK N OK
共用鎖(S) N N OK OK
意向共用鎖(IS) N OK OK OK

按照上面的相容性,如果不同事務之間的鎖相容,則當前加鎖事務可以持有鎖,如果有衝突則會等待其他事務的鎖釋放。

如果一個事務請求鎖時,請求的鎖與已經持有的鎖衝突而無法獲取時,互相等待就可能會產生死鎖。

意向鎖不會阻止除了全表鎖定請求之外的任何鎖請求。
意向鎖的主要目的是顯示事務正在鎖定某行或者正意圖鎖定某行。

3. InnoDB中的鎖

常見的鎖有Record鎖、gap鎖、next-key鎖、插入意向鎖、自增鎖等。
下麵會對每一種鎖給出一個查看鎖的示例。

3.1 準備工作

3.1.1 測試用表結構

示例的基礎是一個只有兩列的資料庫表。

mysql> CREATE TABLE test (
id int(11) NOT NULL,
code int(11) NOT NULL, 
PRIMARY KEY(id), 
KEY (code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 

mysql> INSERT INTO test(id,code) values(1,1),(10,10);

數據表test只有兩列,id是主鍵索引,code是普通的索引(註意,一定不要是唯一索引),並初始化了兩條記錄,分別是(1,1),(10,10)。
這樣,我們驗證唯一鍵索引就可以使用id列,驗證普通索引(非唯一鍵二級索引)時就使用code列。

3.1.2 查看鎖狀態的方式

要看到鎖的情況,必須手動開啟多個事務,其中一些鎖的狀態的查看則必須使鎖處於waiting狀態,這樣才能在mysql的引擎狀態日誌中看到。

命令:

mysql> show engine innodb status;

這條命令能顯示最近幾個事務的狀態、查詢和寫入情況等信息。當出現死鎖時,命令能給出最近的死鎖明細。

3.2 記錄鎖 Record Locks

Record鎖

Record Lock是對索引記錄的鎖定。記錄鎖有兩種模式,S模式和X模式。
例如SELECT id FROM test WHERE id = 10 FOR UPDATE;表示防止任何其他事務插入、更新或者刪除id =10的行。

記錄鎖始終只鎖定索引。即使表沒有建立索引,InnoDB也會創建一個隱藏的聚簇索引(隱藏的遞增主鍵索引),並使用此索引進行記錄鎖定。

查看記錄鎖

開啟第一個事務,不提交,測試完之後回滾。

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> update test set id=2 where id=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

事務加鎖情況

mysql> show engine innodb status\G;
... 
------------
TRANSACTIONS
------------
---TRANSACTION 366811, ACTIVE 690 sec
2 lock struct(s), heap size 1136, 1 row lock(s), undo log entries 2
MySQL thread id 785, OS thread handle 123145432457216, query id 729076 localhost 127.0.0.1 root
...

可以看到有一行被加了鎖。由之前對鎖的描述可以推測出,update語句給id=1這一行上加了一個X鎖

註意:X鎖廣義上是一種抽象意義的排它鎖,即鎖一般分為X模式S模式,狹義上指row或者index上的鎖,而Record鎖是索引上的鎖。
為了不修改數據,可以用select ... for update語句,加鎖行為和updatedelete是一樣的,insert加鎖機制較為複雜,後面的章節會提到。

第一個事務保持原狀,不要提交或者回滾,現在開啟第二個事務。

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> update test set id=3 where id=1;

執行update時,sql語句的執行被阻塞了。查看下事務狀態:

mysql> show engine innodb status\G;
...
------- TRX HAS BEEN WAITING 4 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 62 page no 3 n bits 72 index PRIMARY of table `test`.`test` trx id 366820 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 32
 0: len 8; hex 0000000000000001; asc         ;;
 1: len 6; hex 0000000598e3; asc       ;;
 2: len 7; hex 7e000001a80896; asc ~      ;;

------------------
...

喜聞樂見,我們看到了這個鎖的狀態。狀態標題是'事務正在等待獲取鎖',描述中的lock_mode X locks rec but not gap就是本章節中的record記錄鎖,直譯一下'X鎖模式鎖住了記錄'。後面還有一句but not gap意思是只對record本身加鎖,並不對間隙加鎖,間隙鎖的敘述見下一個章節。

3.3 間隙鎖 Gap Locks

間隙鎖

間隙鎖作用在索引記錄之間的間隔,又或者作用在第一個索引之前,最後一個索引之後的間隙。不包括索引本身。
例如,SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE;這條語句阻止其他事務插入10和20之間的數字,無論這個數字是否存在。

間隙可以跨越0個,單個或多個索引值。

間隙鎖是性能和併發權衡的產物,只存在於部分事務隔離級別。

select * from table where id=1;
唯一索引可以鎖定一行,所以不需要間隙鎖鎖定。
如果列沒有索引或者具有非唯一索引,該語句會鎖定當前索引前的間隙。

在同一個間隙上,不同的事務可以持有上述相容/衝突表中衝突的兩個鎖。例如,事務T1現在持有一個間隙S鎖,T2可以同時在同一個間隙上持有間隙X鎖。
允許衝突的鎖在間隙上鎖定的原因是,如果從索引中清除一條記錄,則由不同事務在這條索引記錄上的加間隙鎖的動作必須被合併。

InnoDB中的間隙鎖的唯一目的是防止其他事務插入間隙。
間隙鎖是可以共存的,一個事務占用的間隙鎖不會阻止另一個事務獲取同一個間隙上的間隙鎖。

如果事務隔離級別改為RC,則間隙鎖會被禁用。

查看間隙鎖

按照官方文檔,where子句查詢條件是唯一鍵且指定了值時,只有record鎖,沒有gap鎖。
如果where語句指定了範圍,gap鎖是存在的。
這裡只測試驗證一下當指定非唯一鍵索引的時候,gap鎖的位置,按照文檔的說法,會鎖定當前索引及索引之前的間隙。(指定了非唯一鍵索引,例如code=10,間隙鎖仍然存在)

開啟第一個事務,鎖定一條非唯一的普通索引記錄

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from test where code = 10 for update;
+----+------+
| id | code |
+----+------+
| 10 |   10 |
+----+------+
1 row in set (0.00 sec)

由於預存了兩條數據,row(1,1)和row(10,10),此時這個間隙應該是1<gap<10。我們先插入row(2,2)來驗證下gap鎖的存在,再插入row(0,0)來驗證gap的邊界。

按照間隙鎖的官方文檔定義,select * from test where code = 10 for update;會鎖定code=10這個索引,並且會鎖定code<10的間隙。

開啟第二個事務,在code=10之前的間隙中插入一條數據,看下這條數據是否能夠插入。

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into test values(2,2);

插入的時候,執行被阻塞,查看引擎狀態:

mysql> show engine innodb status\G;
...
---TRANSACTION 366864, ACTIVE 5 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s), undo log entries 1
MySQL thread id 793, OS thread handle 123145434963968, query id 730065 localhost 127.0.0.1 root update
insert into test values(2,2)
------- TRX HAS BEEN WAITING 5 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 63 page no 4 n bits 72 index code of table `test`.`test` trx id 366864 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 8; hex 800000000000000a; asc         ;;
 1: len 8; hex 000000000000000a; asc         ;;

------------------
...

插入語句被阻塞了,lock_mode X locks gap before rec,由於第一個事務鎖住了1到10之間的gap,需要等待獲取鎖之後才能插入。

如果再開啟一個事務,插入(0,0)

mysql> start transaction;
mysql> insert into test values(0,0);
Query OK, 1 row affected (0.00 sec)

可以看到:指定的非唯一建索引的gap鎖的邊界是當前索引到上一個索引之間的gap

最後給出鎖定區間的示例,首先插入一條記錄(5,5)

mysql> insert into test values(5,5);
Query OK, 1 row affected (0.00 sec)

開啟第一個事務:

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from test where code between 1 and 10 for update;
+----+------+
| id | code |
+----+------+
|  1 |    1 |
|  5 |    5 |
| 10 |   10 |
+----+------+
3 rows in set (0.00 sec)

第二個事務,試圖去更新code=5的行:

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update test set code=4 where code=5;

執行到這裡,如果第一個事務不提交或者回滾的話,第二個事務一直等待直至mysql中設定的超時時間。

3.4 Next-key Locks

Next-key鎖

Next-key鎖實際上是Record鎖和gap鎖的組合。Next-key鎖是在下一個索引記錄本身和索引之前的gap加上S鎖或是X鎖(如果是讀就加上S鎖,如果是寫就加X鎖)。
預設情況下,InnoDB的事務隔離級別為RR,系統參數innodb_locks_unsafe_for_binlog的值為false。InnoDB使用next-key鎖對索引進行掃描和搜索,這樣就讀取不到幻象行,避免了幻讀的發生。

幻讀是指在同一事務下,連續執行兩次同樣的SQL語句,第二次的SQL語句可能會返回之前不存在的行。

當查詢的索引是唯一索引時,Next-key lock會進行優化,降級為Record Lock,此時Next-key lock僅僅作用在索引本身,而不會作用於gap和下一個索引上。

查看Next-key鎖

Next-key鎖的作用範圍

如上述例子,數據表test初始化了row(1,1),row(10,10),然後插入了row(5,5)。數據表如下:

mysql> select * from test;
+----+------+
| id | code |
+----+------+
|  1 |    1 |
|  5 |    5 |
| 10 |   10 |
+----+------+
3 rows in set (0.00 sec)

由於id是主鍵、唯一索引,mysql會做優化,因此使用code這個非唯一鍵的二級索引來舉例說明。

對於code,可能的next-key鎖的範圍是:

(-∞,1]
(1,5]
(5,10]
(10,+∞)

開啟第一個事務,在code=5的索引上請求更新:

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from test where code=5 for update;
+----+------+
| id | code |
+----+------+
|  5 |    5 |
+----+------+
1 row in set (8.81 sec)

之前在gap鎖的章節中介紹了,code=5 for update會在code=5的索引上加一個record鎖,還會在1<gap<5的間隙上加gap鎖。現在不再驗證,直接插入一條(8,8):

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into test values(8);

insert處於等待執行的狀態,這就是next-key鎖生效而導致的結果。第一個事務,鎖定了區間(1,5],由於RR的隔離級別下next-key鎖處於開啟生效狀態,又鎖定了(5,10]區間。所以插入SQL語句的執行被阻塞。

解釋:在這種情況下,被鎖定的區域是code=5前一個索引到它的間隙,以及next-key的區域。code=5 for update對索引的鎖定用區間表示,gap鎖鎖定了(1,5),record鎖鎖定了{5}索引記錄,next-key鎖鎖住了(5,10],也就是說整個(1,10]的區間被鎖定了。由於是for update,所以這裡的鎖都是X鎖,因此阻止了其他事務中帶有衝突鎖定的操作執行。

如果我們在第一個事務中,執行了code>8 for update,在掃描過程中,找到了code=10,此時就會鎖住10之前的間隙(5到10之間的gap),10本身(record),和10之後的間隙(next-key)。此時另一個事務插入(6,6),(9,9)和(11,11)都是不被允許的,只有在前一個索引5及5之前的索引和間隙才能執行插入(更新和刪除也會被阻塞)。

3.5 插入意向鎖 Insert Intention Locks

插入意向鎖在行插入之前由INSERT設置一種間隙鎖,是意向排它鎖的一種。
在多事務同時寫入不同數據至同一索引間隙的時,不會發生鎖等待,事務之間互相不影響其他事務的完成,這和間隙鎖的定義是一致的。

假設一個記錄索引包含4和7,其他不同的事務分別插入5和6,此時只要行不衝突,插入意向鎖不會互相等待,可以直接獲取。參照鎖相容/衝突矩陣。
插入意向鎖的例子不再列舉,可以查看gap鎖的第一個例子。

3.6 自增鎖

自增鎖(AUTO-INC Locks)是事務插入時自增列上特殊的表級別的鎖。最簡單的一種情況:如果一個事務正在向表中插入值,則任何其他事務必須等待,以便第一個事務插入的行接收連續的主鍵值。

我們一般把主鍵設置為AUTO_INCREMENT的列,預設情況下這個欄位的值為0,InnoDB會在AUTO_INCREMENT修飾下的數據列所關聯的索引末尾設置獨占鎖。在訪問自增計數器時,InnoDB使用自增鎖,但是鎖定僅僅持續到當前SQL語句的末尾,而不是整個事務的結束,畢竟自增鎖是表級別的鎖,如果長期鎖定會大大降低資料庫的性能。由於是表鎖,在使用期間,其他會話無法插入表中。

4 幻讀

這一章節,我們通過幻讀,逐步展開對InnoDB鎖的探究。

4.1 幻讀概念

解釋了不同概念的鎖的作用域,我們來看一下幻讀到底是什麼。幻讀在RR條件下是不會出現的。因為RR是Repeatable Read,它是一種事務的隔離級別,直譯過來也就是“在同一個事務中,同樣的查詢語句的讀取是可重覆”,也就是說他不會讀到”幻影行”(其他事務已經提交的變更),它讀到的只能是重覆的(無論在第一次查詢之後其他事務做了什麼操作,第二次查詢結果與第一次相同)。

上面的例子都是使用for update,這種讀取操作叫做當前讀,對於普通的select語句均為快照讀。

當前讀,又叫加鎖讀,或者 阻塞讀。這種讀取操作不再是讀取快照,而是讀取最新版本並且加鎖。
快照讀不會添加任何鎖。

官方文檔對於幻讀的定義是這樣的:

原文:The so-called phantom problem occurs within a transaction when the same query produces different sets of rows at different times. For example, if a SELECT is executed twice, but returns a row the second time that was not returned the first time, the row is a “phantom” row.
手動無腦翻譯:所謂的幻影行問題是指,在同一個事務中,同樣的查詢語句執行多次,得到了不同的結果,這就是幻讀。例如,如果同一個SELECT語句執行了兩次,第二次執行的時候比第一次執行時多出一行,則該行就是所謂的幻影行。

The so-called phantom problem occurs within a transaction when the same query produces different sets of rows at different times.,這句話看起來應該是不可重覆讀的定義,同樣的查詢得到了不同的結果(兩次結果不是重覆的),但是後面的舉例給出了幻讀真正的定義,第二次比第一次多出了一行。也就是說,幻讀的出現有這樣一個前提,第二次查詢前其他事務提交了一個INSERT插入語句。而不可重覆讀出現的前提是第二次查詢前其他事務提交了UPDATE或者DELETE操作。

mysql的快照讀,使得在RR的隔離級別上在next-Key的作用區間內,製造了一個快照副本,這個副本是隔離的,無論副本對應的區間里的數據被其他事務如何修改,在當前事務中,取到的數據永遠是副本中的數據。
RR級別下之所以可以讀到之前版本的數據,是由於資料庫的MVCC(Multi-Version Concurrency Control,多版本併發控制)。參見InnoDB Multi-Versioning

有些文章中提到“RR也不能完全避免幻讀”,實際上官方文檔實際要表達的意義是“在同一個事務內,多次連續查詢的結果是一樣的,不會因其他事務的修改而導致不同的查詢結果”,這裡先給出實驗結論:

1.當前事務如果未發生更新操作(增刪改),快照版本會保持不變,多次查詢讀取的副本是同一個。
2.當前事務如果發生更新(增刪改),再次查詢時,會刷新快照版本。

4.2 RC級別下的幻讀

RC情況下會出現幻讀。
首先設置隔離級別為RC,SET SESSION tx_isolation='READ-COMMITTED';

事務一 事務二
mysql> SET SESSION tx_isolation='READ-COMMITTED';
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test where code > 8;
+----+------+
| id | code |
+----+------+
| 10 | 10 |
+----+------+
1 row in set (0.01 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into test values(9,9);
Query OK, 1 row affected (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test where code > 8;
+----+------+
| id | code |
+----+------+
| 9 | 9 |
+----+------+
| 10 | 10 |
+----+------+
1 row in set (0.01 sec)

RC(Read Commit)隔離級別可以避免臟讀,事務內無法獲取其他事務未提交的變更,但是由於能夠讀到已經提交的事務,因此會出現幻讀和不重覆讀。
也就是說,RC的快照讀是讀取最新版本數據,而RR的快照讀是讀取被next-key鎖作用區域的副本

4.3 RR級別下能否避免幻讀?

我們先來模擬一下RR隔離級別下沒有出現幻讀的情況:

開啟第一個事務並執行一次快照查詢。

事務一 事務二
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test where code > 8;
+----+------+
| id | code |
+----+------+
| 10 | 10 |
+----+------+
1 row in set (0.01 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into test values(9,9);
Query OK, 1 row affected (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test where code > 8;
+----+------+
| id | code |
+----+------+
| 10 | 10 |
+----+------+
1 row in set (0.01 sec)

這兩個事務的執行,有兩個問題:

1.為什麼之前的例子中,在第二個事務的INSERT被阻塞了,而這次卻執行成功了。
這是因為原來的語句中帶有for update,這種讀取是當前讀,會加鎖。而本次第一個事務中的SELECT僅僅是快照讀,沒有加任何鎖。所以不會阻塞其他的插入。

2.資料庫中的數據已經改變,為什麼會讀不到?
這個就是之前提到的next-key lock鎖定的副本。RC及以下級別才會讀到已經提交的事務。更多的業務邏輯是希望在某段時間內或者某個特定的邏輯區間中,前後查詢到的數據是一致的,當前事務是和其他事務隔離的。這也是資料庫在設計實現時遵循的ACID原則。

再給出RR條件下出現幻讀的情形,這種情形不需要兩個事務,一個事務就已經可以說明,

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from test where id>8;
+----+------+
| id | code |
+----+------+
| 10 |   10 |
+----+------+
1 row in set (0.00 sec)

mysql> update test set code=9 where id=10;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * from test where id>8;
+----+------+
| id | code |
+----+------+
| 10 |    9 |
+----+------+
1 row in set (0.00 sec)

至於RR隔離級別下到底會不會出現幻讀,就需要看幻讀的定義中的查詢到底是連續的查詢還是不連續的查詢。如果認為RR級別下可能會出現幻讀,那該級別下也會出現不重覆讀。


RR隔離級別下,雖然不會出現幻讀,但是會因此產生其他的問題。
前提:當前數據表中只存在(1,1),(5,5),(10,10)三組數據。

如果資料庫隔離級別不是預設,可以執行SET SESSION tx_isolation='REPEATABLE-READ';(該語句不是全局設置)更新為RR。

然後執行下列操作:

事務一 事務二 備註
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test where code > 8;
+----+------+
| id | code |
+----+------+
| 10 | 10 |
+----+------+
1 row in set (0.01 sec)
開啟事務一,並查詢code>8的記錄,只有一條(10,10)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into test values(11,11);
Query OK, 1 row affected (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
開啟第二個事務,插入(11,11)並提交
mysql> select * from test where code > 8;
+----+------+
| id | code |
+----+------+
| 10 | 10 |
+----+------+
1 row in set (0.01 sec)
事務一再查詢一次,由於RR級別並沒有讀到更新
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into test values(11,11);
ERROR 1062 (23000): Duplicate entry '11' for key 'PRIMARY'
事務一明明沒有查到,卻插入不了

4.5 更新丟失(Lost Update)

4.5.1 更新丟失

除了上述這類問題外,RR還會有丟失更新的問題。
如下表給出的操作:

事務一 事務二 備註
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test where code > 8;
+----+------+
| id | code |
+----+------+
| 10 | 10 |
+----+------+
1 row in set (0.01 sec)
開啟事務一,並查詢code>8的記錄,只有一條(10,10)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> update test set id=12,code=12 where id=10;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
開啟第二個事務,將(10,10)改為(12,12)並提交,註意這裡matched是1,changed也是1
mysql> select * from test where code > 8;
+----+------+
| id | code |
+----+------+
| 10 | 10 |
+----+------+
1 row in set (0.01 sec)
事務一再次查詢code>8的記錄,仍然只有一條(10,10)
mysql> update test set id=9,code=9 where id=10;
Query OK, 0 row affected (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0 Changed: 0 Warnings: 0
這裡查詢到0條,更新了0條

這個例子里,事務一的更新是無效的,儘管在這個事務里程式認為還存在(10,10)記錄。
事務一中更新之前的SELECT操作是快照讀,所以讀到了快照里的(10,10),而UPDATE中的WHERE子句是當前讀,取得是最新版本的數據,所以matched: 0 Changed: 0

如果上述例子中的操作是對同一條記錄做修改,就會引起更新丟失。例如,事務一和二同時開啟,事務一先執行update test set code=100 where id=10;,事務二再執行update test set code=200 where id=10;,事務一的更新就會被覆蓋。

這就是經典的丟失更新問題,英文叫Lost Update,又叫提交覆蓋,因為是最後執行更新的事務提交導致的覆蓋。還有一種更新丟失叫做回滾覆蓋,即一個事務的回滾把另一個事務提交的數據給回滾覆蓋了,但是目前市面上所有的資料庫都不支持這種stupid的操作,因此不再詳述。

4.5.2 樂觀鎖與悲觀鎖

這種情況下,引入我們常見的兩種方式來解決該問題

  • 樂觀鎖:在UPDATEWHERE子句中加入版本號信息來確定修改是否生效
  • 悲觀鎖:在UPDATE執行前,SELECT後面加上FOR UPDATE來給記錄加鎖,保證記錄在UPDATE前不被修改。SELECT ... FOR UPDATE是加上了X鎖,也可以通過SELECT ... LOCK IN SHARE MODE加上S鎖,來防止其他事務對該行的修改。

無論是樂觀鎖還是悲觀鎖,使用的思想都是一致的,那就是當前讀。樂觀鎖利用當前讀判斷是否是最新版本,悲觀鎖利用當前讀鎖定行。
但是使用樂觀鎖時仍然需要非常謹慎,因為RR是可重覆讀的,一定不能在UPDATE之前先把版本號讀取出來。

5. InnoDB對不同語句執行時的加鎖狀況

如果一個SQL語句要對二級索引(非主鍵索引)設置X模式的Record鎖,InnoDB還會檢索出相應的聚簇索引(主鍵索引)並對它們設置鎖定。

5.1 SELECT ... FROM...不加鎖

SELECT ... FROM是快照讀取,除了SERIALIZABLE的事務隔離級別,該SQL語句執行時不會加任何鎖。

SERIALIZABLE級別下,SELECT語句的執行會在遇到的索引記錄上設置S模式的next-key鎖。但是對於唯一索引,只鎖定索引記錄,而不會鎖定gap。

5.2 UPDATE系列

S鎖讀取(SELECT ... LOCK IN SHARE MODE),X鎖讀取(SELECT ... FOR UPDATE)、更新UPDATE和刪除DELETE這四類語句,採用的鎖取決於搜索條件中使用的索引類型。

  • 如果使用唯一索引,InnoDB僅鎖定索引記錄本身,不鎖定間隙。
  • 如果使用非唯一索引,或者未命中索引,InnoDB使用間隙鎖或者next-key鎖來鎖定索引範圍,這樣就可以阻止其他事務插入鎖定範圍。

5.2.1 UPDATE語句

UPDATE ... WHERE ...在搜索遇到的每條記錄上設置一個獨占的next-key鎖,如果是唯一索引只鎖定記錄。
UPDATE修改聚簇索引時,將對受影響的二級索引採用隱式鎖,隱式鎖是在索引中對二級索引的記錄邏輯加鎖,實際上不產生鎖對象,不占用記憶體空間。

例如update test set code=100 where id=10;執行的時候code=10的索引(code是二級索引,見文中給出的建表語句)會被加隱式鎖,只有隱式鎖產生衝突時才會變成顯式鎖(如S鎖、X鎖)。即此時另一個事務也去更新id=10這條記錄,隱式鎖就會升級為顯示鎖。
這樣做的好處是降低了鎖的開銷。

UPDATE可能會導致新的普通索引的插入。當新的索引插入之前,會首先執行一次重覆索引檢查。在重覆檢查和插入時,更新操作會對受影響的二級索引記錄採用共用鎖定(S鎖)。

5.2.2 DELETE語句

DELETE FROM ... WHERE ...在搜索遇到的每條記錄上設置一個獨占的next-key鎖,如果是唯一索引只鎖定記錄。

5.3 INSERT

INSERT區別於UPDATE系列單獨列出,是因為它的處理方式較為特別。

插入行之前,會設置一種插入意向鎖,插入意向鎖表示插入的意圖。如果其它事務在要插入的位置上設置了X鎖,則無法獲取插入意向鎖,插入操作也因此阻塞。

INSERT在插入的行上設置X鎖。該鎖是一個Record鎖,並不是next-key鎖,即只鎖定記錄本身,不鎖定間隙,因此不會阻止其他會話在這行記錄前的間隙中插入新的記錄。
具體的加鎖過程,見6.2。

6. 可能的死鎖場景

6.1 Duplicate key error引發的死鎖

併發條件下,唯一鍵索引衝突可能會導致死鎖,這種死鎖一般分為兩種,一種是rollback引發,另一種是commit引發。

6.1.1 rollback引發的Duplicate key死鎖

我命名為insert-insert-insert-rollback死鎖

事務一 事務二 事務三
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into test values (2,2);
Query OK, 1 row affected (0.01 sec)
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into test values (2,2);
執行之後被阻塞,等待事務一
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into test values (2,2);
執行之後被阻塞,等待事務一
mysql>rollback;
Query OK, 0 rows affected (0.00 sec)
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
Query OK, 1 row affected (16.13 sec)

當事務一執行回滾時,事務二和事務三發生了死鎖。InnoDB的死鎖檢測一旦檢測到死鎖發生,會自動失敗其中一個事務,因此看到的結果是一個失敗另一個成功。

為什麼會死鎖?

死鎖產生的原因是事務一插入記錄時,對(2,2)記錄加X鎖,此時事務二和事務三插入數據時檢測到了重覆鍵錯誤,此時事務二和事務三要在這條索引記錄上設置S鎖,由於X鎖的存在,S鎖的獲取被阻塞。
事務一回滾,由於S鎖和S鎖是可以相容的,因此事務二和事務三都獲得了這條記錄的S鎖,此時其中一個事務希望插入,則該事務期望在這條記錄上加上X鎖,然而另一個事務持有S鎖,S鎖和X鎖互相是不相容的,兩個事務就開始互相等待對方的鎖釋放,造成了死鎖。

事務二和事務三為什麼會加S鎖,而不是直接等待X鎖

事務一的insert語句加的是隱式鎖(隱式的Record鎖、X鎖),但是其他事務插入同一行記錄時,出現了唯一鍵衝突,事務一的隱式鎖升級為顯示鎖。
事務二和事務三在插入之前判斷到了唯一鍵衝突,是因為插入前的重覆索引檢查,這次檢查必須進行一次當前讀,於是非唯一索引就會被加上S模式的next-key鎖,唯一索引就被加上了S模式的Record鎖。
因為插入和更新之前都要進行重覆索引檢查而執行當前讀操作,所以RR隔離級別下,同一個事務內不連續的查詢,可能也會出現幻讀的效果(但個人並不認為RR級別下也會出現幻讀,幻讀的定義應該是連續的讀取)。而連續的查詢由於都是讀取快照,中間沒有當前讀的操作,所以不會出現幻讀。

6.1.2 commit引發的Duplicate key死鎖

delete-insert-insert-commit死鎖

事務一 事務二 事務三
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> delete from test where id=2;
Query OK, 1 row affected (0.01 sec)
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into test values (2,2);
執行之後被阻塞,等待事務一
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into test values (2,2);
執行之後被阻塞,等待事務一
mysql>commit;
Query OK, 0 rows affected (0.00 sec)
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
Query OK, 1 row affected (2.37 sec)

這種情況下產生的死鎖和insert-insert-insert-rollback死鎖產生的原理一致。

6.2 數據插入的過程

經過以上分析,一條數據在插入時經過以下幾個過程:
假設數據表test.test中存在(1,1)、(5,5)和(10,10)三條記錄。

  • 事務開啟,嘗試獲取插入意向鎖。例如,事務一執行了select * from test where id>8 for update,事務二要插入(9,9),此時先要獲取插入意向鎖,由於事務一已經在對應的記錄和間隙上加了X鎖,因此事務二被阻塞,並且阻塞的原因是獲取插入意向鎖時被事務一的X鎖阻塞。
  • 獲取意向鎖之後,插入之前進行重覆索引檢查。重覆索引檢查為當前讀,需要添加S鎖。
  • 如果是已經存在唯一索引,且索引未加鎖。直接拋出Duplicate key的錯誤。如果存在唯一索引,且索引加鎖,等待鎖釋放。
  • 重覆檢查通過之後,加入X鎖,插入記錄

6.3 GAP與Insert Intention衝突引發死鎖

update-insert死鎖

仍然是表test,當前表中的記錄如下:

mysql> select * from test;
+----+------+
| id | code |
+----+------+
|  1 |    1 |
|  5 |    5 |
| 10 |   10 |
+----+------+
3 rows in set (0.01 sec)
事務一 事務二
begin; begin;
select * from test where id=5 for update; select * from test where id=10 for update;
insert into test values(7,7);
insert into test values(7,7);
Query OK, 1 row affected (5.03 sec)
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

使用show engine innodb status查看死鎖狀態。先後出現lock_mode X locks gap before rec insert intention waitinglock_mode X locks gap before rec字眼,是gap鎖和插入意向鎖的衝突導致的死鎖。

回顧select...for update的加鎖範圍

首先回顧一下兩個事務中的select ... for update做了哪些加鎖操作。

code=5時,首先會獲取code=5的索引記錄鎖(Record鎖),根據之前gap鎖的介紹,會在前一個索引和當前索引之間的間隙加鎖,於是區間(1,5)之間被加上了X模式的gap鎖。除此之外RR模式下,還會加next-key鎖,於是區間(5,10]被加了next-key鎖

  • 因此,code=5的加鎖範圍是,區間(1,5)的gap鎖,{5}索引Record鎖,(5,10]的next-key鎖。即區間(1,10)上都被加上了X模式的鎖。
  • 同理,code=10的加鎖範圍是,區間(5,10)的gap鎖,{10}索引Record鎖,(10,+∞)的next-key鎖。

由gap鎖的特性,相容矩陣中衝突的鎖也可以被不同的事務同時加在一個間隙上。上述兩個select ... for update語句出現了間隙鎖的交集,code=5的next-key鎖和code=10的gap鎖有重疊的區域——(5,10)。

死鎖的成因

當事務一執行插入語句時,會先加X模式的插入意向鎖,即相容矩陣中的IX鎖。
但是由於插入意向鎖要鎖定的位置存在X模式的gap鎖。相容矩陣中IX和X鎖是不相容的,因此事務一的IX鎖會等待事務二的gap鎖釋放。

事務二也執行插入語句,與事務一同樣,事務二的插入意向鎖IX鎖會等待事務一的gap鎖釋放。

兩個事務互相等待對方先釋放鎖,因此出現死鎖。

7 總結

除了以上給出的幾種死鎖模式,還有很多其他死鎖的場景。
無論是哪種場景,萬變不離其宗,都是由於某個區間上或者某一個記錄上可以同時持有鎖,例如不同事務在同一個間隙gap上的鎖不衝突;不同事務中,S鎖可以阻塞X鎖的獲取,但是不會阻塞另一個事務獲取該S鎖。這樣才會出現兩個事務同時持有鎖,並互相等待,最終導致死鎖。

其中需要註意的點是,增、刪、改的操作都會進行一次當前讀操作,以此獲取最新版本的數據,並檢測是否有重覆的索引。
這個過程除了會導致RR隔離級別下出現死鎖之外還會導致其他兩個問題:

  • 第一個是可重覆讀可能會因為這次的當前讀操作而中斷,(同樣,幻讀可能也會因此產生);
  • 第二個是其他事務的更新可能會丟失(解決方式:悲觀鎖、樂觀鎖)。

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 目錄 1. 公鑰私鑰簡介 2. 使用密鑰進行ssh免密登錄 2.1. 實驗環境 2.2. 開始實驗 3. ssh的兩種登陸方式介紹 3.1. 口令驗證登錄 3.2. 密鑰驗證登錄 4. 總結 1、公私鑰簡介與原理 公鑰和私鑰都屬於非對稱加密演算法的一個實現,這個加密演算法的信息交換過程是: 1) 持有公 ...
  • 前言 1. 格式: command [-options] [parameter] 2. 註意:區分大小寫 3. Ctrl + C: 終止當前操作(Tip:若想刪除當前命令行的命令,但內容過多,用Backspace刪除鍵十分麻煩,可用 Ctrl + C 直接彈出新的命令行) 4. 幫助: comman ...
  • 1.用wget下載python源碼 "PYTHON下載" 找適合自己的版本,我下載的是3.7.2 2.用tar命令解壓下載的文件 3.進入目錄解壓後的目錄,創建Makefile文件,執行configure命令,生成Makefile文件 因為我下載的是centos minimal版本,安裝時出現如下錯 ...
  • 是不是經常會遇到需要觀察某個內容,為了持續觀察而反覆執行某個命令?也許你可以寫一個簡單的腳本幫你反覆執行,但是有更簡單方便的方法為什麼不用呢? ...
  • ...
  • 一、HBase表的基本結構 1.概述: 平時常見的MySQL、Oracle資料庫都是傳統型關係資料庫,它們都是按行查詢、按行存儲;而HBase為非關係型資料庫,它是按列存儲的的。 在HBase插入數據時,先輸入數據的格式為rowkey => info => column => value rowke ...
  • SqlServer中: 目標表存在: INSERT INTO 目標表 SELECT * FROM 原表; 目標表不存在:SELECT * INTO 目標表 FROM 原表; Oracle中: 目標表存在:INSERT INTO 目標表 SELECT * FROM 原表; 目標表不存在:CREATE ...
  • Step1.配置本地yum源,方便安裝依賴包 補充: df命令查看 linux系統磁碟空間以及使用情況,-h代表方便閱讀方式顯示 ;/dev/sr0為光碟機設備名 補充: 將光碟機掛在到 /mnt/cdrom下 補充:本地yum源,enabled為1代表啟用 Redhat修改了redhat.repo之後 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...