Next-key locking是如何解决幻读问题的
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Next-key locking是如何解决幻读问题的相关的知识,希望对你有一定的参考价值。
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)
修改参数,并重新启动服务
[[email protected] ~]# vim /etc/my.cnf innodb_locks_unsafe_for_binlog=1 [[email protected] ~]# systemctl restart mariadb [[email protected] ~]# 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/
本文出自 “74567456” 博客,请务必保留此出处http://74567456.blog.51cto.com/11657485/1887690
以上是关于Next-key locking是如何解决幻读问题的的主要内容,如果未能解决你的问题,请参考以下文章
眼见为实自己动手实践理解REPEATABLE READ && Next-Key Lock
MySQL锁--03---意向锁(Intention Locks)间隙锁(Gap Locks)临键锁(Next-Key Locks)
记录锁、间隙锁、Next-key 锁、Insert Intention Locks 和 AUTO-INC Locks 是 Intention Locks 的一部分吗?