文章總共分爲五個部分:html
大而全版(五合一):InnoDB的鎖機制淺析(All in One)mysql
InnoDB常見的鎖有Record鎖、gap鎖、next-key鎖、插入意向鎖、自增鎖等。
下面會對每一種鎖給出一個查看鎖的示例。sql
常見的鎖有Record鎖、gap鎖、next-key鎖、插入意向鎖、自增鎖等。
下面會對每一種鎖給出一個查看鎖的示例。數據庫
示例的基礎是一個只有兩列的數據庫表。併發
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列。性能
要看到鎖的狀況,必須手動開啓多個事務,其中一些鎖的狀態的查看則必須使鎖處於waiting
狀態,這樣才能在mysql的引擎狀態日誌中看到。測試
命令:優化
mysql> show engine innodb status;
這條命令能顯示最近幾個事務的狀態、查詢和寫入狀況等信息。當出現死鎖時,命令能給出最近的死鎖明細。ui
Record Lock
是對索引記錄的鎖定。記錄鎖有兩種模式,S模式和X模式。
例如SELECT id FROM test WHERE id = 10 FOR UPDATE;
表示防止任何其餘事務插入、更新或者刪除id =10
的行。spa
記錄鎖始終只鎖定索引。即便表沒有創建索引,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
語句,加鎖行爲和update
、delete
是同樣的,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自己加鎖,並不對間隙加鎖,間隙鎖的敘述見下一個章節。
間隙鎖做用在索引記錄之間的間隔,又或者做用在第一個索引以前,最後一個索引以後的間隙。不包括索引自己。
例如,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中設定的超時時間。
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和下一個索引上。
如上述例子,數據表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以前的索引和間隙才能執行插入(更新和刪除也會被阻塞)。
插入意向鎖在行插入以前由INSERT設置一種間隙鎖,是意向排它鎖的一種。
在多事務同時寫入不一樣數據至同一索引間隙的時,不會發生鎖等待,事務之間互相不影響其餘事務的完成,這和間隙鎖的定義是一致的。
假設一個記錄索引包含4和7,其餘不一樣的事務分別插入5和6,此時只要行不衝突,插入意向鎖不會互相等待,能夠直接獲取。參照鎖兼容/衝突矩陣。
插入意向鎖的例子再也不列舉,能夠查看gap鎖的第一個例子。
自增鎖(AUTO-INC Locks)是事務插入時自增列上特殊的表級別的鎖。最簡單的一種狀況:若是一個事務正在向表中插入值,則任何其餘事務必須等待,以便第一個事務插入的行接收連續的主鍵值。
咱們通常把主鍵設置爲AUTO_INCREMENT
的列,默認狀況下這個字段的值爲0,InnoDB會在AUTO_INCREMENT
修飾下的數據列所關聯的索引末尾設置獨佔鎖。在訪問自增計數器時,InnoDB使用自增鎖,可是鎖定僅僅持續到當前SQL語句的末尾,而不是整個事務的結束,畢竟自增鎖是表級別的鎖,若是長期鎖定會大大下降數據庫的性能。因爲是表鎖,在使用期間,其餘會話沒法插入表中。