InnoDB的鎖機制淺析(All in One)

InnoDB的鎖機制淺析

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

若是想分開看能夠點擊下面連接:sql

文章總共分爲五個部分:數據庫

1. 前言

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

MySQL數據庫提供了四種默認的隔離級別,讀未提交(read-uncommitted)、讀已提交(或不可重複讀)(read-committed)、可重複讀(repeatable-read)、串行化(serializable)。性能

MySQL的默認隔離級別是RR。測試

2. 鎖基本概念

2.1 共享鎖和排它鎖

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

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

S鎖

若是事務T1持有了行r上的S鎖,則其餘事務能夠同時持有行r的S鎖,可是不能對行r加X鎖spa

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,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 code=5 for update; select * from test where code=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隔離級別下出現死鎖以外還會致使其餘兩個問題:

  • 第一個是可重複讀可能會由於此次的當前讀操做而中斷,(一樣,幻讀可能也會所以產生);
  • 第二個是其餘事務的更新可能會丟失(解決方式:悲觀鎖、樂觀鎖)。
相關文章
相關標籤/搜索