本文轉自: https://www.cnblogs.com/zhoujinyi/p/3435982.html
數據庫使用鎖是爲了支持更好的併發,提供數據的完整性和一致性。InnoDB是一個支持行鎖的存儲引擎,鎖的類型有:共享鎖(S)、排他鎖(X)、意向共享(IS)、意向排他(IX)。爲了提供更好的併發,InnoDB提供了非鎖定讀:不需要等待訪問行上的鎖釋放,讀取行的一個快照。該方法是通過InnoDB的一個特性:MVCC來實現的。
InnoDB有三種行鎖的算法:
1,Record Lock:單個行記錄上的鎖。
2,Gap Lock:間隙鎖,鎖定一個範圍,但不包括記錄本身。GAP鎖的目的,是爲了防止同一事務的兩次當前讀,出現幻讀的情況。
3,Next-Key Lock:1+2,鎖定一個範圍,並且鎖定記錄本身。對於行的查詢,都是採用該方法,主要目的是解決幻讀的問題。
測試一:默認RR隔離級別
root@localhost : test 10:56:10>create table t(a int,key idx_a(a))engine =innodb; Query OK, 0 rows affected (0.20 sec) root@localhost : test 10:56:13>insert into t values(1),(3),(5),(8),(11); Query OK, 5 rows affected (0.00 sec) Records: 5 Duplicates: 0 Warnings: 0 root@localhost : test 10:56:15>select * from t; +------+ | a | +------+ | 1 | | 3 | | 5 | | 8 | | 11 | +------+ 5 rows in set (0.00 sec) section A: root@localhost : test 10:56:27>start transaction; Query OK, 0 rows affected (0.00 sec) root@localhost : test 10:56:29>select * from t where a = 8 for update; +------+ | a | +------+ | 8 | +------+ 1 row in set (0.00 sec) section B: root@localhost : test 10:54:50>begin; Query OK, 0 rows affected (0.00 sec) root@localhost : test 10:56:51>select * from t; +------+ | a | +------+ | 1 | | 3 | | 5 | | 8 | | 11 | +------+ 5 rows in set (0.00 sec) root@localhost : test 10:56:54>insert into t values(2); Query OK, 1 row affected (0.00 sec) root@localhost : test 10:57:01>insert into t values(4); Query OK, 1 row affected (0.00 sec) ++++++++++ root@localhost : test 10:57:04>insert into t values(6); root@localhost : test 10:57:11>insert into t values(7); root@localhost : test 10:57:15>insert into t values(9); root@localhost : test 10:57:33>insert into t values(10); ++++++++++ 上面全被鎖住,阻塞住了 root@localhost : test 10:57:39>insert into t values(12); Query OK, 1 row affected (0.00 sec)
問題:
爲什麼section B上面的插入語句會出現鎖等待的情況?InnoDB是行鎖,在section A裏面鎖住了a=8的行,其他應該不受影響。why?
分析:
因爲InnoDB對於行的查詢都是採用了Next-Key Lock的算法,鎖定的不是單個值,而是一個範圍(GAP)。上面索引值有1,3,5,8,11,其記錄的GAP的區間如下:是一個左開右閉的空間(原因是默認主鍵的有序自增的特性,結合後面的例子說明)
(-∞,1],(1,3],(3,5],(5,8],(8,11],(11,+∞)
特別需要注意的是,InnoDB存儲引擎還會對輔助索引下一個鍵值加上gap lock。如上面分析,那就可以解釋了。
root@localhost : test 10:56:29>select * from t where a = 8 for update; +------+ | a | +------+ | 8 | +------+ 1 row in set (0.00 sec)
該SQL語句鎖定的範圍是(5,8],下個下個鍵值範圍是(8,11],所以插入5~11之間的值的時候都會被鎖定,要求等待。即:插入5,6,7,8,9,10 會被鎖住。插入非這個範圍內的值都正常。
################################### 2016-07-21 更新
因爲例子裏沒有主鍵,所以要用隱藏的ROWID來代替,數據根據Rowid進行排序。而Rowid是有一定順序的(自增),所以其中11可以被寫入,5不能被寫入,不清楚的可以再看一個有主鍵的例子:
會話1: 01:43:07>create table t(id int,name varchar(10),key idx_id(id),primary key(name))engine =innodb; Query OK, 0 rows affected (0.02 sec) 01:43:11>insert into t values(1,'a'),(3,'c'),(5,'e'),(8,'g'),(11,'j'); Query OK, 5 rows affected (0.01 sec) Records: 5 Duplicates: 0 Warnings: 0 01:44:03>select @@global.tx_isolation, @@tx_isolation; +-----------------------+-----------------+ | @@global.tx_isolation | @@tx_isolation | +-----------------------+-----------------+ | REPEATABLE-READ | REPEATABLE-READ | +-----------------------+-----------------+ 1 row in set (0.01 sec) 01:44:58>select * from t; +------+------+ | id | name | +------+------+ | 1 | a | | 3 | c | | 5 | e | | 8 | g | | 11 | j | +------+------+ 5 rows in set (0.00 sec) 01:45:07>start transaction; 01:45:09>delete from t where id=8; Query OK, 1 row affected (0.01 sec) 會話2: 01:50:38>select @@global.tx_isolation, @@tx_isolation; +-----------------------+-----------------+ | @@global.tx_isolation | @@tx_isolation | +-----------------------+-----------------+ | REPEATABLE-READ | REPEATABLE-READ | +-----------------------+-----------------+ 1 row in set (0.01 sec) 01:50:48>start transaction; 01:50:51>select * from t; +------+------+ | id | name | +------+------+ | 1 | a | | 3 | c | | 5 | e | | 8 | g | | 11 | j | +------+------+ 5 rows in set (0.01 sec) 01:51:35>insert into t(id,name) values(6,'f'); ^CCtrl-C -- sending "KILL QUERY 9851" to server ... Ctrl-C -- query aborted. ERROR 1317 (70100): Query execution was interrupted 01:53:32>insert into t(id,name) values(5,'e1'); ^CCtrl-C -- sending "KILL QUERY 9851" to server ... Ctrl-C -- query aborted. ERROR 1317 (70100): Query execution was interrupted 01:53:41>insert into t(id,name) values(7,'h'); ^CCtrl-C -- sending "KILL QUERY 9851" to server ... Ctrl-C -- query aborted. ERROR 1317 (70100): Query execution was interrupted 01:54:43>insert into t(id,name) values(8,'gg'); ^CCtrl-C -- sending "KILL QUERY 9851" to server ... Ctrl-C -- query aborted. ERROR 1317 (70100): Query execution was interrupted 01:55:10>insert into t(id,name) values(9,'k'); ^CCtrl-C -- sending "KILL QUERY 9851" to server ... Ctrl-C -- query aborted. ERROR 1317 (70100): Query execution was interrupted 01:55:23>insert into t(id,name) values(10,'p'); ^CCtrl-C -- sending "KILL QUERY 9851" to server ... Ctrl-C -- query aborted. ERROR 1317 (70100): Query execution was interrupted 01:55:33>insert into t(id,name) values(11,'iz'); ^CCtrl-C -- sending "KILL QUERY 9851" to server ... Ctrl-C -- query aborted. ERROR 1317 (70100): Query execution was interrupted #########上面看到 id:5,6,7,8,9,10,11都被鎖了。 #########下面看到 id:5,11 還是可以插入的 01:54:33>insert into t(id,name) values(5,'cz'); Query OK, 1 row affected (0.01 sec) 01:55:59>insert into t(id,name) values(11,'ja'); Query OK, 1 row affected (0.01 sec)
分析:因爲會話1已經對id=8的記錄加了一個X鎖,由於是RR隔離級別,INNODB要防止幻讀需要加GAP鎖:即id=5(8的左邊),id=11(8的右邊)之間需要加間隙鎖(GAP)。這樣[5,e]和[8,g],[8,g]和[11,j]之間的數據都要被鎖。上面測試已經驗證了這一點,根據索引的有序性,數據按照主鍵(name)排序,後面寫入的[5,cz]([5,e]的左邊)和[11,ja]([11,j]的右邊)不屬於上面的範圍從而可以寫入。
另外一種情況,把name主鍵去掉會是怎麼樣的情況?有興趣的同學可以測試一下。
##################################################
繼續:插入超時失敗後,會怎麼樣?
超時時間的參數:innodb_lock_wait_timeout ,默認是50秒。
超時是否回滾參數:innodb_rollback_on_timeout 默認是OFF。
section A: root@localhost : test 04:48:51>start transaction; Query OK, 0 rows affected (0.00 sec) root@localhost : test 04:48:53>select * from t where a = 8 for update; +------+ | a | +------+ | 8 | +------+ 1 row in set (0.01 sec) section B: root@localhost : test 04:49:04>start transaction; Query OK, 0 rows affected (0.00 sec) root@localhost : test 04:49:07>insert into t values(12); Query OK, 1 row affected (0.00 sec) root@localhost : test 04:49:13>insert into t values(10); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction root@localhost : test 04:50:06>select * from t; +------+ | a | +------+ | 1 | | 3 | | 5 | | 8 | | 11 | | 12 | +------+ 6 rows in set (0.00 sec)
經過測試,不會回滾超時引發的異常,當參數innodb_rollback_on_timeout 設置成ON時,則可以回滾,會把插進去的12回滾掉。
默認情況下,InnoDB存儲引擎不會回滾超時引發的異常,除死鎖外。
既然InnoDB有三種算法,那Record Lock什麼時候用?還是用上面的列子,把輔助索引改成唯一屬性的索引。
測試二:
root@localhost : test 04:58:49>create table t(a int primary key)engine =innodb; Query OK, 0 rows affected (0.19 sec) root@localhost : test 04:59:02>insert into t values(1),(3),(5),(8),(11); Query OK, 5 rows affected (0.00 sec) Records: 5 Duplicates: 0 Warnings: 0 root@localhost : test 04:59:10>select * from t; +----+ | a | +----+ | 1 | | 3 | | 5 | | 8 | | 11 | +----+ 5 rows in set (0.00 sec) section A: root@localhost : test 04:59:30>start transaction; Query OK, 0 rows affected (0.00 sec) root@localhost : test 04:59:33>select * from t where a = 8 for update; +---+ | a | +---+ | 8 | +---+ 1 row in set (0.00 sec) section B: root@localhost : test 04:58:41>start transaction; Query OK, 0 rows affected (0.00 sec) root@localhost : test 04:59:45>insert into t values(6); Query OK, 1 row affected (0.00 sec) root@localhost : test 05:00:05>insert into t values(7); Query OK, 1 row affected (0.00 sec) root@localhost : test 05:00:08>insert into t values(9); Query OK, 1 row affected (0.00 sec) root@localhost : test 05:00:10>insert into t values(10); Query OK, 1 row affected (0.00 sec)
問題:
爲什麼section B上面的插入語句可以正常,和測試一不一樣?
分析:
因爲InnoDB對於行的查詢都是採用了Next-Key Lock的算法,鎖定的不是單個值,而是一個範圍,按照這個方法是會和第一次測試結果一樣。但是,當查詢的索引含有唯一屬性的時候,Next-Key Lock 會進行優化,將其降級爲Record Lock,即僅鎖住索引本身,不是範圍。
注意:通過主鍵或則唯一索引來鎖定不存在的值,也會產生GAP鎖定。即:
會話1: 04:22:38>show create table t\G *************************** 1. row *************************** Table: t Create Table: CREATE TABLE `t` ( `id` int(11) NOT NULL, `name` varchar(10) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 1 row in set (0.00 sec) 04:22:49>start transaction; 04:23:16>select * from t where id = 15 for update; Empty set (0.00 sec) 會話2: 04:26:10>insert into t(id,name) values(10,'k'); Query OK, 1 row affected (0.01 sec) 04:26:26>insert into t(id,name) values(12,'k'); ^CCtrl-C -- sending "KILL QUERY 9851" to server ... Ctrl-C -- query aborted. ERROR 1317 (70100): Query execution was interrupted 04:29:32>insert into t(id,name) values(16,'kxx'); ^CCtrl-C -- sending "KILL QUERY 9851" to server ... Ctrl-C -- query aborted. ERROR 1317 (70100): Query execution was interrupted 04:29:39>insert into t(id,name) values(160,'kxx'); ^CCtrl-C -- sending "KILL QUERY 9851" to server ... Ctrl-C -- query aborted. ERROR 1317 (70100): Query execution was interrupted
如何讓測試一不阻塞?可以顯式的關閉Gap Lock:
1:把事務隔離級別改成:Read Committed,提交讀、不可重複讀。SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
2:修改參數:innodb_locks_unsafe_for_binlog 設置爲1。
總結:
本文只對 Next-Key Lock 做了一些說明測試,關於鎖還有很多其他方面的知識,可以查閱相關資料進行學習。
寫完之後的幾天剛好牛人寫了一篇詳細的文章:http://hedengcheng.com/?p=771
~~~~~~~~~~~~~~~ 萬物之中,希望至美 ~~~~~~~~~~~~~~~