Next-key locking是如何解決幻讀(當前讀)問題的


http://blog.51cto.com/74567456/1887690

類似的文章:https://www.cnblogs.com/zhoujinyi/p/3435982.html

總結:

1. 通過實踐闡述了gap lock 的開啟與關閉:

 A. 將事務隔離級別設置為RC 

B. 將參數innodb_locks_unsafe_for_binlog設置為1

C. 確保where索引唯一 ,從而避讓 gap lock

 

2. 闡述了一些概念:

認識鎖的算法

 

nnoDB存儲引擎的鎖的算法有三種:

  • Record lock:單個行記錄上的鎖

  • Gap lock:間隙鎖,鎖定一個范圍,不包括記錄本身

  • Next-key lock:record+gap 鎖定一個范圍,包含記錄本身

Lock的精度(type)分為 行鎖、表鎖、意向鎖

Lock的模式(mode)分為:

  • 鎖的類型 ——【讀鎖和寫鎖】或者【共享鎖和排他鎖】即 【X or S】

  • 鎖的范圍 ——【record lock、gap lock、Next-key lock】

知識點

  1. innodb對於行的查詢(rr級別的當前讀)使用next-key lock

  2. Next-locking keying為了解決Phantom Problem幻讀問題

  3. 當查詢的索引含有唯一屬性時,將next-key lock降級為record key

  4. Gap鎖設計的目的是為了阻止多個事務將記錄插入到同一范圍內,而這會導致幻讀問題的產生

  5. 有兩種方式顯式關閉gap鎖:(除了外鍵約束和唯一性檢查外,其余情況僅使用record lock) A. 將事務隔離級別設置為RC B. 將參數innodb_locks_unsafe_for_binlog設置為1

     

lock18

 

 

 

 

 

 

 

Next-key locking是如何解決幻讀問題的

 

lock19

首先什么是幻讀呢?

舉個例子,兩個男孩同時在追求一個女生的故事

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  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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記錄。

1
2
3
4
5
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  

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#事務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】

知識點

  1. innodb對於行的查詢使用next-key lock

  2. Next-locking keying為了解決Phantom Problem幻讀問題

  3. 當查詢的索引含有唯一屬性時,將next-key lock降級為record key

  4. Gap鎖設計的目的是為了阻止多個事務將記錄插入到同一范圍內,而這會導致幻讀問題的產生

  5. 有兩種方式顯式關閉gap鎖:(除了外鍵約束和唯一性檢查外,其余情況僅使用record lock) A. 將事務隔離級別設置為RC B. 將參數innodb_locks_unsafe_for_binlog設置為1

     

lock18

實踐1: 驗證next-key lock降級為record key

創建db1.t1表,有列a和b,分別為char(10)和int型,並且b為key,注意b列為索引列,但並不是主鍵,因此不是唯一的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#事務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鎖的信息

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
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列是唯一的。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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;

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#事務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的行,顯式加共享鎖。

1
2
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鎖的信息

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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格式,否則執行顯式加鎖時會報錯

 

1
2
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;

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#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參數的值

 

1
2
3
4
5
6
7
MariaDB [(none)]>  select  @@innodb_locks_unsafe_for_binlog;
+----------------------------------+
| @@innodb_locks_unsafe_for_binlog |
+----------------------------------+
|                                0 |
+----------------------------------+
1 row  in  set  (0.00 sec)

 

修改參數,並重新啟動服務

 

1
2
3
4
5
6
7
8
9
10
[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;

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#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/

 



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM