Next-key locking是如何解决幻读问题的

InnoDB锁详解

 

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 RCT2 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 RRT2 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 RRT2 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 RCT2 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 RRT2 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/

转载于:https://my.oschina.net/newchaos/blog/1580256

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值