Next-key locking是如何解決幻讀問題的
首先什麼是幻讀呢?
舉個例子,兩個男孩同時在追求一個女生的故事
A問:你有男朋友嗎?女孩對他說沒有。A追求女孩的事件還沒有提交,就是繼續追求哈。
就在A追求的同時,B也在追求,並且直接讓女孩做他的女朋友,女孩答應了,B的追求事件結束。
A又問:你有男朋友嗎? 女孩對他說我已經有男朋友了! 嗚嗚嗚 !剛纔你還沒有的,怎麼現在就有了呢?
女孩說,你也沒說過你追我的時候不讓別人追我啊!... ... A哭着走了。
幻讀 Phantom Problem 是指在同一事務下,連續執行兩次相同的sql語句可能導致不同的結果,第二次的sql語句可能會返回之前不存在的行。
在剛纔我舉的例子裏,A雖然問了女孩有沒有男朋友,但是沒有告訴女孩,在他追求時,不可以接受別人的追求,所以悲催的結局。
那麼A怎麼才能在他追求事件結束前讓女孩不答應別人的追求呢?
innodb中的RR隔離級別是通過next-key locking是如何解決幻讀問題的,就是鎖住一個範圍。
那麼如果你是A你怎麼做呢?你肯定要跟女孩說,只要我開始追求你,問了你有沒有男朋友,在我結束追求你之前,你不可以答應別人的追求!我要把你腦子裏記錄男朋友的區域全部鎖起來,啊哈啊!
下面我們來做一個測試,分別在RR和RC隔離級別中來實現:
測試使用表db1.t1 (a int primary key) ,記錄有1,3,5
T1 RC | T2 RR |
---|---|
begin; | begin; |
set session transaction isolation level READ COMMITTED; | |
select * from db1.t1 where a>3 for update; | |
查詢結果爲5 | |
insert into db1.t1 values (4); | |
commit; | |
select * from db1.t1 where a>3; | |
查詢結果爲4 5 |
MariaDB [db1]> create table t1 (a int primary key); Query OK, 0 rows affected (0.22 sec) MariaDB [db1]> insert into t1 values (1),(3),(5); Query OK, 3 rows affected (0.02 sec) Records: 3 Duplicates: 0 Warnings: 0 #事務T1 MariaDB [db1]> begin; Query OK, 0 rows affected (0.00 sec) MariaDB [db1]> set session transaction isolation level read co Query OK, 0 rows affected (0.01 sec) MariaDB [db1]> select * from db1.t1 where a>3 for update; +---+ | a | +---+ | 5 | +---+ 1 row in set (0.01 sec) #事務T2 MariaDB [db1]> begin; Query OK, 0 rows affected (0.00 sec) MariaDB [db1]> insert into db1.t1 values (4); Query OK, 1 row affected (0.00 sec) MariaDB [db1]> commit; Query OK, 0 rows affected (0.03 sec) #事務T1 MariaDB [db1]> select * from db1.t1 where a>3 for update; +---+ | a | +---+ | 4 | | 5 | +---+ 2 rows in set (0.00 sec)
將會話中的隔離界別改爲RR,並刪除a=4記錄。
MariaDB [db1]> set session transaction isolation level repeatable read; Query OK, 0 rows affected (0.00 sec) MariaDB [db1]> delete from db1.t1 where a=4; Query OK, 1 row affected (0.00 sec)
T1 RR | T2 RR |
---|---|
begin; | begin; |
select * from db1.t1 where a>3 for update; | |
查詢結果爲5 | |
insert into db1.t1 values (4); | |
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction | |
commit; | |
select * from db1.t1 where a>3; | |
查詢結果爲5 |
#事務T1 MariaDB [(none)]> begin; Query OK, 0 rows affected (0.00 sec) MariaDB [(none)]> select * from db1.t1 where a>3 for update; +---+ | a | +---+ | 5 | +---+ 1 row in set (0.02 sec) #事務T2 MariaDB [(none)]> begin; Query OK, 0 rows affected (0.00 sec) MariaDB [(none)]> insert into db1.t1 values (4); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction MariaDB [(none)]> commit; Query OK, 0 rows affected (0.00 sec) #事務T1 MariaDB [(none)]> select * from db1.t1 where a>3 for update; +---+ | a | +---+ | 5 | +---+ 1 row in set (0.02 sec)
認識鎖的算法
nnoDB存儲引擎的鎖的算法有三種:
Record lock:單個行記錄上的鎖
Gap lock:間隙鎖,鎖定一個範圍,不包括記錄本身
Next-key lock:record+gap 鎖定一個範圍,包含記錄本身
Lock的精度(type)分爲 行鎖、表鎖、意向鎖
Lock的模式(mode)分爲:
鎖的類型 ——【讀鎖和寫鎖】或者【共享鎖和排他鎖】即 【X or S】
鎖的範圍 ——【record lock、gap lock、Next-key lock】
知識點
innodb對於行的查詢使用next-key lock
Next-locking keying爲了解決Phantom Problem幻讀問題
當查詢的索引含有唯一屬性時,將next-key lock降級爲record key
Gap鎖設計的目的是爲了阻止多個事務將記錄插入到同一範圍內,而這會導致幻讀問題的產生
有兩種方式顯式關閉gap鎖:(除了外鍵約束和唯一性檢查外,其餘情況僅使用record lock) A. 將事務隔離級別設置爲RC B. 將參數innodb_locks_unsafe_for_binlog設置爲1
實踐1: 驗證next-key lock降級爲record key
創建db1.t1表,有列a和b,分別爲char(10)和int型,並且b爲key,注意b列爲索引列,但並不是主鍵,因此不是唯一的。
MariaDB [db1]> create table db1.t1 (a char(10),b int,key (b)); Query OK, 0 rows affected (0.03 sec) MariaDB [db1]> insert into db1.t1 values ('batman',1),('superman',3),('leo',5); Query OK, 3 rows affected (0.15 sec) Records: 3 Duplicates: 0 Warnings: 0 MariaDB [db1]> select * from db1.t1; +----------+------+ | a | b | +----------+------+ | batman | 1 | | superman | 3 | | leo | 5 | +----------+------+ 3 rows in set (0.02 sec)
接下來開啓兩個事務T1和T2,T1中查看b=3的行,顯式加排他鎖;T1未提交事務時,T2事務開啓並嘗試插入新行a='batman',b=2和a='batman',b=4;
#事務T1 MariaDB [db1]> begin; Query OK, 0 rows affected (0.00 sec) MariaDB [db1]> select * from db1.t1 where b=3 for update; +----------+------+ | a | b | +----------+------+ | superman | 3 | +----------+------+ 1 row in set (0.12 sec) #事務T2 MariaDB [db1]> begin; Query OK, 0 rows affected (0.00 sec) MariaDB [db1]> insert into db1.t1 values ('batman',2); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction MariaDB [db1]> insert into db1.t1 values ('batman',4); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
發現T2事務中不能插入新行a='batman',b=2和a='batman',b=4;可以查看當前innodb鎖的信息
MariaDB [db1]> select * from information_schema.innodb_locks\G; *************************** 1. row *************************** lock_id: 111B:0:334:3 lock_trx_id: 111B lock_mode: X,GAP lock_type: RECORD lock_table: `db1`.`t1` lock_index: `b` lock_space: 0 lock_page: 334 lock_rec: 3 lock_data: 3, 0x00000000020E *************************** 2. row *************************** lock_id: 111A:0:334:3 lock_trx_id: 111A lock_mode: X lock_type: RECORD lock_table: `db1`.`t1` lock_index: `b` lock_space: 0 lock_page: 334 lock_rec: 3 lock_data: 3, 0x00000000020E 2 rows in set (0.01 sec) ERROR: No query specified MariaDB [db1]> select * from information_schema.innodb_lock_waits\G; *************************** 1. row *************************** requesting_trx_id: 111B requested_lock_id: 111B:0:334:3 blocking_trx_id: 111A blocking_lock_id: 111A:0:334:3 1 row in set (0.09 sec) MariaDB [db1]> select * from information_schema.innodb_lock_waits\G; *************************** 1. row *************************** requesting_trx_id: 111B requested_lock_id: 111B:0:334:4 blocking_trx_id: 111A blocking_lock_id: 111A:0:334:4 1 row in set (0.00 sec) ERROR: No query specified MariaDB [db1]> select * from information_schema.innodb_locks\G; *************************** 1. row *************************** lock_id: 111B:0:334:4 lock_trx_id: 111B lock_mode: X,GAP lock_type: RECORD lock_table: `db1`.`t1` lock_index: `b` lock_space: 0 lock_page: 334 lock_rec: 4 lock_data: 5, 0x00000000020F *************************** 2. row *************************** lock_id: 111A:0:334:4 lock_trx_id: 111A lock_mode: X,GAP lock_type: RECORD lock_table: `db1`.`t1` lock_index: `b` lock_space: 0 lock_page: 334 lock_rec: 4 lock_data: 5, 0x00000000020F 2 rows in set (0.11 sec) ERROR: No query specified
我們看到T2事務的兩次插入動作都在請求排他鎖,但是此時T1事務已經在加了next-key lock(record + gap),表現範圍爲b的(1,5),包括記錄3,所以T2事務在T1事務解鎖之間,不能插入到b的(1,5)範圍內
* lock_mode: X,GAP
lock_mode 可以理解爲 讀鎖還是寫鎖?
;是在什麼範圍上鎖?
;此處加的寫鎖即排他鎖;範圍是(1,5)
* lock_type: RECORD
表示鎖的精度,根據存儲引擎不同,innodb是行鎖,MYISAM是表鎖
刪除db1.t1表,重新創建db1.t1表,有列a和b,分別爲char(10)和int型,並且b爲primay key,因此b列是唯一的。
MariaDB [db1]> drop tables t1; Query OK, 0 rows affected (0.12 sec) MariaDB [db1]> create table db1.t1 (a char(10),b int ,primary key (b)); Query OK, 0 rows affected (0.02 sec) MariaDB [db1]> insert into db1.t1 values ('batman',1),('superman',3),('leo',5); Query OK, 3 rows affected (0.12 sec) Records: 3 Duplicates: 0 Warnings: 0 MariaDB [db1]> select * from db1.t1; +----------+---+ | a | b | +----------+---+ | batman | 1 | | superman | 3 | | leo | 5 | +----------+---+ 3 rows in set (0.08 sec)
接下來開啓兩個事務T1和T2,T1中查看b=3的行,顯式加排他鎖;T1未提交事務時,T2事務開啓並嘗試插入新行a='batman',b=2和a='batman',b=4;
#事務T1 MariaDB [db1]> begin; Query OK, 0 rows affected (0.00 sec) MariaDB [db1]> select * from db1.t1 where b=3 for update; +----------+---+ | a | b | +----------+---+ | superman | 3 | +----------+---+ 1 row in set (0.14 sec) #事務T2 MariaDB [db1]> begin; Query OK, 0 rows affected (0.00 sec) MariaDB [db1]> insert into db1.t1 values ('batman',2); Query OK, 1 row affected (0.00 sec) MariaDB [db1]> insert into db1.t1 values ('batman',4); Query OK, 1 row affected (0.00 sec)
繼續在T2事務中嘗試查看b=3的行,顯式加共享鎖。
#事務T2 MariaDB [db1]> select * from db1.t1 where b=3 lock in share mode; ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
發現T2事務中可以插入新行a='batman',b=2和a='batman',b=4;但是不能查看b=3的行,接下來我們查看當前innodb鎖的信息
MariaDB [db1]> select * from information_schema.innodb_locks\G; *************************** 1. row *************************** lock_id: 1122:0:337:3 lock_trx_id: 1122 lock_mode: S lock_type: RECORD lock_table: `db1`.`t1` lock_index: `PRIMARY` lock_space: 0 lock_page: 337 lock_rec: 3 lock_data: 3 *************************** 2. row *************************** lock_id: 1121:0:337:3 lock_trx_id: 1121 lock_mode: X lock_type: RECORD lock_table: `db1`.`t1` lock_index: `PRIMARY` lock_space: 0 lock_page: 337 lock_rec: 3 lock_data: 3 2 rows in set (0.02 sec) ERROR: No query specified MariaDB [db1]> select * from information_schema.innodb_lock_waits\G; *************************** 1. row *************************** requesting_trx_id: 1122 requested_lock_id: 1122:0:337:3 blocking_trx_id: 1121 blocking_lock_id: 1121:0:337:3 1 row in set (0.00 sec) ERROR: No query specified
從以上信息可以看到,T1事務當前只在b=3所在的行上加了寫鎖,排他鎖,並沒有同時使用gap鎖來組成next-key lock。
到此,已經證明了,當查詢的索引含有唯一屬性時,將next-key lock降級爲record key
我們第二次創建的t1表的列b是主鍵,而主鍵必須是唯一的。
實踐2: 關閉GAP鎖_RC
有兩種方式顯式關閉gap鎖:(除了外鍵約束和唯一性檢查外,其餘情況僅使用record lock)
A. 將事務隔離級別設置爲RC B. 將參數innodb_locks_unsafe_for_binlog設置爲1
T1 RR | T2 RR |
---|---|
begin; | begin; |
select * from db1.t1 where b=3 for update; | |
insert into db1.t1 values ('batman',2) | |
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction | |
set session transaction isolation level READ COMMITTED; | |
commit; | commit; |
注意,將T1事務設置爲RC後,需要將二進制日誌的格式改爲row格式,否則執行顯式加鎖時會報錯
MariaDB [db1]> insert into t1 values ('batman',2); ERROR 1665 (HY000): Cannot execute statement: impossible to write to binary log since BINLOG_FORMAT = STATEMENT and at least one table uses a storage engine limited to row-based logging. InnoDB is limited to row-logging when transaction isolation level is READ COMMITTED or READ UNCOMMITTED.
T1 RC | T2 RR |
---|---|
begin; | begin; |
set session transaction isolation level READ COMMITTED; | |
select * from db1.t1 where b=3 for update; | |
insert into db1.t1 values ('batman',2) | |
insert into db1.t1 values ('batman',4) | |
commit; | commit; |
#T1事務 MariaDB [db1]> set session transaction isolation level READ COMMITTED; Query OK, 0 rows affected (0.00 sec) MariaDB [db1]> select @@tx_isolation; +----------------+ | @@tx_isolation | +----------------+ | READ-COMMITTED | +----------------+ 1 row in set (0.00 sec) MariaDB [db1]> begin; Query OK, 0 rows affected (0.09 sec) MariaDB [db1]> select * from t1 where b=3 for update; +----------+------+ | a | b | +----------+------+ | superman | 3 | +----------+------+ 1 row in set (0.00 sec) #T2事務 MariaDB [db1]> begin; Query OK, 0 rows affected (0.16 sec) MariaDB [db1]> select @@tx_isolation; +----------------+ | @@tx_isolation | +----------------+ | READ-COMMITTED | +----------------+ 1 row in set (0.00 sec) MariaDB [db1]> insert into db1.t1 values ('batman',2); Query OK, 1 row affected (0.00 sec) MariaDB [db1]> commit; Query OK, 0 rows affected (0.01 sec) MariaDB [db1]> set session transaction isolation level REPEATABLE READ; Query OK, 0 rows affected (0.00 sec) MariaDB [db1]> select @@tx_isolation; +-----------------+ | @@tx_isolation | +-----------------+ | REPEATABLE-READ | +-----------------+ 1 row in set (0.00 sec) MariaDB [db1]> begin; Query OK, 0 rows affected (0.00 sec) MariaDB [db1]> insert into db1.t1 values ('batman',4); Query OK, 1 row affected (0.00 sec) MariaDB [db1]> commit; Query OK, 0 rows affected (0.00 sec) #T1事務 MariaDB [db1]> commit; Query OK, 0 rows affected (0.00 sec)
我在做測試的時候,T1事務隔離界別爲RC,T2事務的隔離界別分別用RC和RR做了測試,都是可以的
實踐3: 關閉GAP鎖_innodb_locks_unsafe_for_binlog
查看當前innodb_locks_unsafe_for_binlog參數的值
MariaDB [(none)]> select @@innodb_locks_unsafe_for_binlog; +----------------------------------+ | @@innodb_locks_unsafe_for_binlog | +----------------------------------+ | 0 | +----------------------------------+ 1 row in set (0.00 sec)
修改參數,並重新啓動服務
[root@localhost ~]# vim /etc/my.cnf innodb_locks_unsafe_for_binlog=1 [root@localhost ~]# systemctl restart mariadb [root@localhost ~]# mysql -e "select @@innodb_locks_unsafe_for_binlog" +----------------------------------+ | @@innodb_locks_unsafe_for_binlog | +----------------------------------+ | 1 | +----------------------------------+
還是去創建db1.t1表,如果已有就先drop;有列a和b,分別爲char(10)和int型,並且b爲key,注意b列爲索引列,但並不是主鍵,因此不是唯一的。
T1 RR | T2 RR |
---|---|
begin; | begin; |
select * from db1.t1 where b=3 for update; | |
insert into db1.t1 values ('batman',2) | |
insert into db1.t1 values ('batman',4) | |
commit; | commit; |
MariaDB [db1]> create table db1.t1 (a char(10),b int,key (b)); Query OK, 0 rows affected (0.03 sec) MariaDB [db1]> insert into db1.t1 values ('batman',1),('superman',3),('leo',5); Query OK, 3 rows affected (0.15 sec) Records: 3 Duplicates: 0 Warnings: 0 MariaDB [db1]> select * from db1.t1; +----------+------+ | a | b | +----------+------+ | batman | 1 | | superman | 3 | | leo | 5 | +----------+------+ 3 rows in set (0.02 sec)
接下來開啓兩個事務T1和T2,T1中查看b=3的行,顯式加排他鎖;T1未提交事務時,T2事務開啓並嘗試插入新行a='batman',b=2和a='batman',b=4;
#T1事務 MariaDB [(none)]> begin; Query OK, 0 rows affected (0.00 sec) MariaDB [(none)]> select * from db1.t1 where b=3 for update; +----------+------+ | a | b | +----------+------+ | superman | 3 | +----------+------+ 1 row in set (0.01 sec) #T2事務 MariaDB [(none)]> begin; Query OK, 0 rows affected (0.00 sec) MariaDB [(none)]> insert into db1.t1 values ('batman',4); Query OK, 1 row affected (0.01 sec) MariaDB [(none)]> insert into db1.t1 values ('batman',2); Query OK, 1 row affected (0.00 sec) MariaDB [(none)]> commit; Query OK, 0 rows affected (0.00 sec) #T1事務 MariaDB [(none)]> commit; Query OK, 0 rows affected (0.00 sec)
不轉行的女工程師 https://booboowei.github.io/