设为首页 收藏本站
查看: 502|回复: 0

[经验分享] Next-key locking是如何解决幻读问题的

[复制链接]
累计签到:1 天
连续签到:1 天
发表于 2017-1-3 09:19:21 | 显示全部楼层 |阅读模式
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
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】

知识点
  • innodb对于行的查询使用next-key lock
  • Next-locking keying为了解决Phantom Problem幻读问题
  • 当查询的索引含有唯一属性时,将next-key lock降级为record key
  • Gap锁设计的目的是为了阻止多个事务将记录插入到同一范围内,而这会导致幻读问题的产生
  • 有两种方式显式关闭gap锁:(除了外键约束和唯一性检查外,其余情况仅使用record lock)A. 将事务隔离级别设置为RCB. 将参数innodb_locks_unsafe_for_binlog设置为1



实践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. 将事务隔离级别设置为RCB. 将参数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
[iyunv@localhost ~]# vim /etc/my.cnf
innodb_locks_unsafe_for_binlog=1
[iyunv@localhost ~]# systemctl restart mariadb

[iyunv@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)






运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-323077-1-1.html 上篇帖子: linux中指令下达与快速编辑按键 下篇帖子: Linux 系统监控诊断命令—lsof locking 如何
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表