Mysql加鎖過程詳解(8)-理解innodb的鎖(record,gap,Next-Key lock) Mysql加鎖過程詳解(8)-理解innodb的鎖(record,gap,Next-Key lo

 

 

Record lock

單條索引記錄上加鎖,record lock鎖住的永遠是索引,而非記錄自己,即便該表上沒有任何索引,那麼innodb會在後臺建立一個隱藏的彙集主鍵索引,那麼鎖住的就是這個隱藏的彙集主鍵索引。因此說當一條sql沒有走任何索引時,那麼將會在每一條彙集索引後面加X鎖,這個相似於表鎖,但原理上和表鎖應該是徹底不一樣的。

html

Gap lock

在索引記錄之間的間隙中加鎖,或者是在某一條索引記錄以前或者以後加鎖,並不包括該索引記錄自己。gap lock的機制主要是解決可重複讀模式下的幻讀問題,關於幻讀的演示和gap鎖如何解決了幻讀。關於這一塊,先給出幾個定義

mysql

快照讀:

簡單的select操做,沒有lock in share mode或for update,快照讀不會加任何的鎖,並且因爲mysql的一致性非鎖定讀的機制存在,任何快照讀也不會被阻塞。可是若是事務的隔離級別是SERIALIZABLE的話,那麼快照讀也會被加上共享的next-key鎖,本文不對SERIALIZABLE隔離級別作敘述。
程序員

當前讀:

官方文檔的術語叫locking read,也就是insert,update,delete,select..in share mode和select..for update,當前讀會在全部掃描到的索引記錄上加鎖無論它後面的where條件到底有沒有命中對應的行記錄。當前讀可能會引發死鎖。sql

意向鎖:

innodb的意向鎖主要用戶多粒度的鎖並存的狀況。好比事務A要在一個表上加S鎖,若是表中的一行已被事務B加了X鎖,那麼該鎖的申請也應被阻塞。若是表中的數據不少,逐行檢查鎖標誌的開銷將很大,系統的性能將會受到影響。爲了解決這個問題,能夠在表級上引入新的鎖類型來表示其所屬行的加鎖狀況,這就引出了「意向鎖」的概念數據庫

舉個例子,若是表中記錄1億,事務A把其中有幾條記錄上了行鎖了,這時事務B須要給這個表加表級鎖,若是沒有意向鎖的話,那就要去表中查找這一億條記錄是否上鎖了。若是存在乎向鎖,那麼假如事務A在更新一條記錄以前,先加意向鎖,再加X鎖,事務B先檢查該表上是否存在乎向鎖,存在的意向鎖是否與本身準備加的鎖衝突,若是有衝突,則等待直到事務A釋放,而無須逐條記錄去檢測。事務B更新表時,其實無須知道到底哪一行被鎖了,它只要知道反正有一行被鎖了就好了。session


說白了意向鎖的主要做用是處理行鎖和表鎖之間的矛盾,可以顯示「某個事務正在某一行上持有了鎖,或者準備去持有鎖」
併發

 

下文之知乎大神觀點:https://www.zhihu.com/question/51513268ide

innodb的意向鎖有什麼做用? mysql官網上對於意向鎖的解釋中有這麼一句話
「The main purpose of IX and IS locks is to show that someone is locking a row, or going to lock a row in the table.」
意思是說加意向鎖的目的是爲了代表某個事務正在鎖定一行或者將要鎖定一行。
那麼,意向鎖的做用就是「代表」加鎖的意圖,但是爲何要代表這個 意圖呢?
若是僅僅鎖定一行僅僅須要加一個鎖,那麼就直接加鎖就行了,這裏要代表加鎖意圖的緣由是由於要鎖定一行不只僅是要加一個鎖,而是要作一系列操做嗎? 做者:尹發條地精 我最近也在看這個,我說一下個人理解
①在mysql中有表鎖,LOCK TABLE my_tabl_name READ; 用讀鎖鎖表,會阻塞其餘事務修改表數據。LOCK TABLE my_table_name WRITe; 用寫鎖鎖表,會阻塞其餘事務讀和寫。
②Innodb引擎又支持行鎖,行鎖分爲共享鎖,一個事務對一行的共享只讀鎖。排它鎖,一個事務對一行的排他讀寫鎖。
③這兩中類型的鎖共存的問題考慮這個例子:
事務A鎖住了表中的一行,讓這一行只能讀,不能寫。以後,事務B申請整個表的寫鎖。若是事務B申請成功,那麼理論上它就能修改表中的任意一行,這與A持有的行鎖是衝突的。
數據庫須要避免這種衝突,就是說要讓B的申請被阻塞,直到A釋放了行鎖。

數據庫要怎麼判斷這個衝突呢?
step1:判斷表是否已被其餘事務用表鎖鎖表
step2:判斷表中的每一行是否已被行鎖鎖住。
注意step2,這樣的判斷方法效率實在不高,由於須要遍歷整個表。
因而就有了意向鎖。在乎向鎖存在的狀況下,事務A必須先申請表的意向共享鎖,成功後再申請一行的行鎖。在乎向鎖存在的狀況下,
上面的判斷能夠改爲
step1:不變
step2:發現表上有意向共享鎖,說明表中有些行被共享行鎖鎖住了,所以,事務B申請表的寫鎖會被阻塞。

注意:申請意向鎖的動做是數據庫完成的,就是說,事務A申請一行的行鎖的時候,數據庫會自動先開始申請表的意向鎖,不須要咱們程序員使用代碼來申請。

總結:爲了實現多粒度鎖機制(白話:爲了表鎖和行鎖都能用)

 

 

不可重複讀:

指的是在同一個事務中,連續幾回快照讀,讀取的記錄應該是同樣的post

不可重複讀的演示較爲簡單,本文不作討論。性能

幻讀:

指的是在一個事務A中執行了一個當前讀操做,而另一個事務B在事務A的影響區間內insert了一條記錄,這時事務A再執行一個當前讀操做時,出現了幻行。這和不可重複讀的主要區別就在與事務A中一個是快照讀,一個當前讀;而且事務B中一個是任何的dml操做,一個只是insert。好比在A中select * from test where id<10 lock in share mode結果集爲(1,2,3),這時在B中對test表插入了一條記錄4,這時在A中從新查詢結果集就是(1,2,3,4),和事務A在第一次查詢出來的結果集不一致,這裏的4就是幻行。

演示條件:因爲可重讀的隔離級別下,默認採用Next-Key Locks,就是Record lock和gap lock的結合,即除了鎖住記錄自己,還要再鎖住索引之間的間隙,因此這個gap lock機制默認打開,並不會產生幻行,那麼咱們要演示幻行的話,要麼將隔離級別改成read-commited,要麼在REPEATABLE-READ模式下禁用掉gap lock,這裏咱們採用的是第二種方式。


幻讀的演示

在演示以前又引入了innodb_locks_unsafe_for_binlog參數,該參數能夠禁用gap lock。

innodb_locks_unsafe_for_binlog:靜態參數,默認爲0,表示啓動gap lock,若是設置爲1,表示禁用gap lock,這時mysql就只有record lock了,不過值得注意的是,即便了設置了1,關於外鍵和惟一鍵重複檢查方面用到的gap lock依舊有效。這時能夠簡單地理解成事務的隔離級別退化成可重複讀,而後二者應該仍是有所區別的。建議是不要隨便設置,咱們這裏設置只是作個簡單的幻讀演示,mysql後續的版本可能都會廢棄掉這個參數了。


session 1 先將myid>95的記錄加一個當前讀
mysql> show create table test_gap_lock\G
*************************** 1. row ***************************
       Table: test_gap_lock
Create Table: CREATE TABLE `test_gap_lock` (
  `id` int(11) NOT NULL,
  `name` varchar(100) DEFAULT NULL,
  `myid` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uniq_name` (`name`),
  KEY `idex_myid` (`myid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
1 row in set (0.00 sec)
mysql> begin;
mysql> select * from test_gap_lock where myid>95 for update;
+----+------------+------+
| id | name       | myid |
+----+------------+------+
|  1 | jiang      |   99 |
|  2 | hubingmei  |   99 |
|  5 | hubingmei4 |  100 |
+----+------------+------+
3 rows in set (0.00 sec)


session 2  這時session 2插入myid=98的記錄成功了。
insert into test_gap_lock values(6,'jiang2',98);
Query OK, 1 row affected (0.00 sec)


session 1 這時session 1再次查看時發現記錄myid=98的記錄已經存在了,這條記錄就是幻行。
mysql> select * from test_gap_lock where myid>95 for update;
+----+------------+------+
| id | name       | myid |
+----+------------+------+
|  1 | jiang      |   99 |
|  2 | hubingmei  |   99 |
|  5 | hubingmei4 |  100 |
|  6 | jiang2     |   98 |
+----+------------+------+
4 rows in set (0.00 sec)

gap lock機制解決幻讀問題演示

條件:咱們再把innodb_locks_unsafe_for_binlog值改回默認值0,而且tx_isolation爲REPEATABLE-READ,演示時務必explain下,確保該sql走了非惟一索引idx_myid(由於若是測試數據較少的話,可能優化器直接走全表掃描,那就致使鎖住全部記錄,沒法模擬出gap鎖)

演示範例 1(非惟一索引+範圍當前讀)

mysql> show create table test_gap_lock\G
*************************** 1. row ***************************
       Table: test_gap_lock
Create Table: CREATE TABLE `test_gap_lock` (
  `id` int(11) NOT NULL,
  `name` varchar(100) DEFAULT NULL,
  `myid` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uniq_name` (`name`),
  KEY `idex_myid` (`myid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
1 row in set (0.00 sec)


session 1 先explain確保session的當前讀sql執行走了索引idx_myid
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> explain select * from test_gap_lock where myid>100 for update;
+----+-------------+---------------+-------+---------------+-----------+---------+------+------+-----------------------+
| id | select_type | table         | type  | possible_keys | key       | key_len | ref  | rows | Extra                 |
+----+-------------+---------------+-------+---------------+-----------+---------+------+------+-----------------------+
|  1 | SIMPLE      | test_gap_lock | range | idex_myid     | idex_myid | 5       | NULL |    2 | Using index condition |
+----+-------------+---------------+-------+---------------+-----------+---------+------+------+-----------------------+
1 row in set (0.00 sec)
mysql>  select * from test_gap_lock where myid>100 for update;
+----+------------+------+
| id | name       | myid |
+----+------------+------+
|  5 | hubingmei4 |  101 |
| 98 | test       |  105 |
+----+------------+------+
2 rows in set (0.00 sec)


session 2 先插入myid=56的成功,由於鎖住的間隙是myid>100,56不在該範圍內;再插入myid=109時,會一直卡住直到session 1commit,rollback或者直接鎖等待超時,在鎖等待超時前在session 1中執行一樣的sql,獲得的結果依舊只有id=5,98的記錄,這樣就避免了幻讀問題
mysql> insert into test_gap_lock values(999,'test2',56);
Query OK, 1 row affected (0.00 sec)
mysql> insert into test_gap_lock values(123,'test4',109);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction



演示範例2(非惟一索引+等值當前讀)

mysql> select * from test_gap_lock;
+-----+------------+------+
| id  | name       | myid |
+-----+------------+------+
|   1 | jiang      |   98 |
|   2 | hubingmei  |   99 |
|   5 | hubingmei4 |  101 |
|   6 | jiang2     |  100 |
|   7 | jiang22    |   70 |
|  67 | jiang222   |   80 |
|  98 | test       |  105 |
| 123 | test4      |  109 |
| 999 | test2      |   56 |
+-----+------------+------+
9 rows in set (0.00 sec)
session 1
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> explain delete from test_gap_lock where myid=100;
+----+-------------+---------------+-------+---------------+-----------+---------+-------+------+-------------+
| id | select_type | table         | type  | possible_keys | key       | key_len | ref   | rows | Extra       |
+----+-------------+---------------+-------+---------------+-----------+---------+-------+------+-------------+
|  1 | SIMPLE      | test_gap_lock | range | idex_myid     | idex_myid | 5       | const |    2 | Using where |
+----+-------------+---------------+-------+---------------+-----------+---------+-------+------+-------------+
1 row in set (0.00 sec)
mysql> delete from test_gap_lock where myid=100;
Query OK, 2 rows affected (0.00 sec)


session 2 插入myid=99的記錄依舊阻塞,存在gap鎖;插入myid=97的記錄成功
mysql> insert into test_gap_lock values(676,'gap recored test',99);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into test_gap_lock values(675,'gap recored test1',97);
Query OK, 1 row affected (0.00 sec)

範例3(主鍵索引+範圍當前讀)



mysql> select * from test_gap_lock ;
+-----+------------+------+
| id  | name       | myid |
+-----+------------+------+
|   1 | jiang      |   98 |
|   2 | hubingmei  |   98 |
|   5 | hubingmei4 |  100 |
|   6 | jiang2     |  100 |
|   7 | jiang22    |   70 |
|  67 | jiang222   |   80 |
|  98 | test       |  105 |
| 123 | test4      |  109 |
| 999 | test2      |   56 |
+-----+------------+------+
9 rows in set (0.00 sec)
session 1 
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> explain select * from test_gap_lock where id > 100 for update;
+----+-------------+---------------+-------+---------------+---------+---------+------+------+-------------+
| id | select_type | table         | type  | possible_keys | key     | key_len | ref  | rows | Extra       |
+----+-------------+---------------+-------+---------------+---------+---------+------+------+-------------+
|  1 | SIMPLE      | test_gap_lock | range | PRIMARY       | PRIMARY | 4       | NULL |    2 | Using where |
+----+-------------+---------------+-------+---------------+---------+---------+------+------+-------------+
1 row in set (0.00 sec)
mysql>  select * from test_gap_lock where id > 100 for update;
+-----+-------+------+
| id  | name  | myid |
+-----+-------+------+
| 123 | test4 |  109 |
| 999 | test2 |   56 |
+-----+-------+------+
2 rows in set (0.00 sec)


session 2(id=3可插入;id=108沒法插入,存在gap lock;id=123的記錄沒法select..in share mode,由於該記錄上存在record lock;id=125能夠被select..in share mode和update,這點比較奇怪,應該這也算是當前讀,不事後來查看官方文檔得知,gap鎖只會阻塞insert操做,由於gap間隙中是不存在任何記錄的,除了insert操做,其餘的操做結果應該都等價於空操做,mysql就不去阻塞它了)
mysql> insert into test_gap_lock values(108,'gap lock test3',123);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into test_gap_lock values(3,'gap lock test3',123);
Query OK, 1 row affected (0.00 sec)
mysql> select * from test_gap_lock where id=125 lock in share mode;
Empty set (0.00 sec)
mysql> explain select * from test_gap_lock where id=125 lock in share mode;
+----+-------------+-------+------+---------------+------+---------+------+------+-----------------------------------------------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows | Extra                                               |
+----+-------------+-------+------+---------------+------+---------+------+------+-----------------------------------------------------+
|  1 | SIMPLE      | NULL  | NULL | NULL          | NULL | NULL    | NULL | NULL | Impossible WHERE noticed after reading const tables |
+----+-------------+-------+------+---------------+------+---------+------+------+-----------------------------------------------------+
1 row in set (0.00 sec)
mysql> update test_gap_lock set myid=12345 where id=125;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0  Changed: 0  Warnings: 0



gap lock的內部加鎖原理

gap lock的前置條件:

1 事務隔離級別爲REPEATABLE-READ,innodb_locks_unsafe_for_binlog參數爲0,且sql走的索引爲非惟一索引

2 事務隔離級別爲REPEATABLE-READ,innodb_locks_unsafe_for_binlog參數爲0,且sql是一個範圍的當前讀操做,這時即便不是非惟一索引也會加gap lock

gap lock的加鎖步驟

針對上面的範例1(非惟一索引+範圍當前讀)和範例3(主鍵索引+範圍當前讀)比較好理解,那爲何範例2(非主鍵索引+等值當前讀)爲何也會產生gap lock,這要從btree 索引的原理講起,咱們都知道,btree索引是按照順序排列的,而且innodb存在主鍵彙集索引,本人繪圖能力有限,已範例2的加鎖過程分析舉例,手寫加鎖過程以下圖


從圖中的數據組織順序能夠看出,myid=100的記錄有兩條,若是加gap鎖就會產生三個間隙,分別是gap1(98,100),gap2(100,100),gap3(100,105),在這三個開區間(若是我高中數學沒記錯的話)內的myid數值沒法插入,顯然gap1還有(myid=99,id=3)(myid

=99,id=4)等記錄,gap2無實際的間隙,gap3還有(myid=101,id=7)等記錄。而且,在myid=100的兩條記錄上加了record lock,也就是這兩條數據業務沒法被其餘session進行當前讀操做(範例三能夠看出)


Next-Key Locks

在默認狀況下,mysql的事務隔離級別是可重複讀,而且innodb_locks_unsafe_for_binlog參數爲0,這時默認採用next-key locks。所謂Next-Key Locks,就是Record lock和gap lock的結合,即除了鎖住記錄自己,還要再鎖住索引之間的間隙。

下面咱們針對大部分的SQL類型分析是如何加鎖的,假設事務隔離級別爲可重複讀

select .. from  

不加任何類型的鎖

select...from lock in share mode

在掃描到的任何索引記錄上加共享的(shared)next-key lock,還有主鍵彙集索引加排它鎖 

select..from for update

在掃描到的任何索引記錄上加排它的next-key lock,還有主鍵彙集索引加排它鎖 

update..where   delete from..where

在掃描到的任何索引記錄上加next-key lock,還有主鍵彙集索引加排它鎖 

insert into..

簡單的insert會在insert的行對應的索引記錄上加一個排它鎖,這是一個record lock,並無gap,因此並不會阻塞其餘session在gap間隙裏插入記錄。不過在insert操做以前,還會加一種鎖,官方文檔稱它爲insertion intention gap lock,也就是意向的gap鎖。這個意向gap鎖的做用就是預示着當多事務併發插入相同的gap空隙時,只要插入的記錄不是gap間隙中的相同位置,則無需等待其餘session就可完成,這樣就使得insert操做無須加真正的gap lock。想象一下,若是一個表有一個索引idx_test,表中有記錄1和8,那麼每一個事務均可以在2和7之間插入任何記錄,只會對當前插入的記錄加record lock,並不會阻塞其餘session插入與本身不一樣的記錄,由於他們並無任何衝突。

假設發生了一個惟一鍵衝突錯誤,那麼將會在重複的索引記錄上加讀鎖。當有多個session同時插入相同的行記錄時,若是另一個session已經得到改行的排它鎖,那麼將會致使死鎖。

insert致使的死鎖現象演示1

mysql> show create table t1\G
*************************** 1. row ***************************
       Table: t1
Create Table: CREATE TABLE `t1` (
  `i` int(11) NOT NULL DEFAULT '0',
  PRIMARY KEY (`i`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
1 row in set (0.00 sec)

session 1 

mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> INSERT INTO t1 VALUES(1);
Query OK, 1 row affected (0.00 sec)


session 2 這時session2一直被卡住

mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> INSERT INTO t1 VALUES(1);


session 3 這時session3也一直被卡住

mysql> begin;

Query OK, 0 rows affected (0.00 sec)

mysql> INSERT INTO t1 VALUES(1);


session 1 這時咱們回滾session1

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


發現session 2的insert成功,而session3檢測到死鎖回滾

session 2 Query OK, 1 row affected (28.87 sec)

session 3  ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

 死鎖緣由分析:

首先session1插入一條記錄,得到該記錄的排它鎖,這時session2和session3都檢測到了主鍵衝突錯誤,可是因爲session1並無提交,因此session1並不算插入成功,因而它並不能直接報錯吧,因而session2和session3都申請了該記錄的共享鎖,這時還沒獲取到共享鎖,處於等待隊列中。這時session1 rollback了,也就釋放了該行記錄的排它鎖,那麼session2和session3都獲取了該行上的共享鎖。而session2和session3想要插入記錄,必須獲取排它鎖,但因爲他們本身都擁有了共享鎖,因而永遠沒法獲取到排它鎖,因而死鎖就發生了。若是這時session1是commit而不是rollback的話,那麼session2和session3都直接報錯主鍵衝突錯誤。查看死鎖日誌也是一目瞭然



insert致使的死鎖現象2

另一個相似的死鎖是session1刪除了id=1的記錄並未提交,這時session2和session3插入id=1的記錄。這時session1 commit了,session2和session3須要insert的話,就須要獲取排它鎖,那麼死鎖也就發生了;session1 rollback,則session2和session3報錯主鍵衝突。這裏再也不作演示。


INSERT ... ON DUPLICATE KEY UPDATE

這種sql和insert加鎖的不一樣的是,若是檢測到鍵衝突,它直接申請加排它鎖,而不是共享鎖。

replace

replace操做若是沒有檢測到鍵衝突的話,那麼它的加鎖策略和insert類似;若是檢測到鍵衝突,那麼它也是直接再申請加排它鎖

INSERT INTO T SELECT ... FROM S WHERE ...


在T表上的加鎖策略和普通insert一致,另外還會在S表上的相關記錄上加共享的next-key lock。(若是是可重複讀模式,則不會加鎖)

CREATE TABLE ... SELECT ...

在select的表上加共享的next-key lock

自增id的加鎖策略

當一張表的某個字段是自增列時,innodb會在該索引的末位加一個排它鎖。爲了訪問這個自增的數值,須要加一個表級鎖,不過這個表級鎖的持續時間只有當前sql,而不是整個事務,即當前sql執行完,該表級鎖就釋放了。其餘session沒法在這個表級鎖持有時插入任何記錄。

外鍵檢測的加鎖策略

若是存在外鍵約束,任何的insert,update,delete將會檢測約束條件,將會在相應的記錄上加共享的record lock,不管是否存在外鍵衝突。
相關文章
相關標籤/搜索