MySQL事务隔离级别与锁的关系(InnoDB存储引擎)

时间:2022-09-17 23:18:19

SQL标准定义了四个隔离级别:READ UNCOMMITTED(读未提交)、READ COMMITTED(读已提交)、REPEATABLE READ(可重读)、SERIALIZABLE(序列化)。此处只针对InnoDB引擎讨论READ COMMITTED与REPEATABLE READ的异同。

        在READCOMMITTED隔离级别下,同一事务中进行多次相同的查询,会得到不同的查询结果(也就是”不可重复读”);在REPEATABLE READ隔离级别下,同一事务中进行多次相同的查询,会得到相同的查询结果(也就是“可重复读”)。请看下面这个例子:

首先准备如下数据:

create table tx_test(id int ,KEY (`id`))ENGINE=InnoDB;

INSERT INTO tx_test select 1;

INSERT INTO tx_test select 3;

INSERT INTO tx_test select 5;

INSERT INTO tx_test select 7;

然后按下图顺序执行两个事务:

 MySQL事务隔离级别与锁的关系(InnoDB存储引擎)

那么,在两种隔离级别下分别会出现什么结果呢?下面细细道来。

在READCOMMITTED隔离级别下:

会话A: BEGIN;

会话A: SELECT * from tx_test where id>5 FOR UPDATE;

执行结果:7

会话B:INSERT INTO tx_test select 6;

执行结果:插入成功

会话A:SELECT * from tx_test where id>5 FOR UPDATE;

执行结果:6,7(两行)

会话A:COMMIT;

在REPEATABLEREAD隔离界别下:

会话A: BEGIN;

会话A: SELECT * from tx_test where id>5 FOR UPDATE;

执行结果:7

会话B:INSERT INTO tx_test select 6;

执行结果:阻塞,等待会话A释放锁

会话A:SELECT * from tx_test where id>5 FOR UPDATE;

执行结果:7(一行)

会话A:COMMIT

 

为什么发生以上的差异呢?因为InnoDB在READ COMMITTED隔离级别下使用Record Lock,在REPEATABLE READ隔离级别下使用Next-Key Lock。

         InnoDB存储引擎有三种锁的算法,其分别是:

Record Lock:单行记录上的锁

                            对于数据1,3,5,7

                            执行SELECT *from tx_test where id=5 FOR UPDATE;,会锁定id=5这一行

                            执行SELECT *from tx_test where num>3 FOR UPDATE,会锁定id=5,7两行

                            执行SELECT *from tx_test where num=4 FOR UPDATE,不会锁定任何一行

Gap Lock:间隙锁,锁定一个范围,但不包含记录本身

                            对于数据1,3,5,7

                            执行SELECT *from tx_test where id=5 FOR UPDATE;,锁定范围(3,5)

                            执行SELECT *from tx_test where num>3 FOR UPDATE,锁定范围(3,+∞)

                            执行SELECT *from tx_test where num=4 FOR UPDATE,锁定范围(3,5)

 

Next-Key Lock:Record Lock与Gap Lock的组合,锁定一个范围,并且锁定记录本身

                            对于数据1,3,5,7

                            执行SELECT *from tx_test where id=5 FOR UPDATE;,锁定范围(3,5]

                            执行SELECT *from tx_test where num>3 FOR UPDATE,锁定范围(3,+∞)

                            执行SELECT *from tx_test where num=4 FOR UPDATE,锁定范围(3,5)

 

 

结合以上三种算法的定义,就不难解释前面提到的例子产生的原因。

在READ COMMITTED隔离级别下,会话A执行SELECT * from tx_test where id>5 FOR UPDATE 使用的是RecordLock,那么只会锁定该语句扫描出的一行记录7,因此会话B执行INSERT INTO tx_test select 6不存在等待锁的情况。

在REPEATABLE READ隔离级别下,会话A执行SELECT * from tx_test where id>5 FOR UPDATE 使用的是Next-KeyLock,那么会锁定一个范围(5,+∞),因此会话B执行INSERT INTO tx_test select 6需要等待会话A释放锁。

 

现在讨论第二种情况:

表数据1,3,5,7,隔离级别REPEATABLE READ,会话A执行SELECT * from tx_test where id=5 FOR UPDATE,然后会话B执行INSERT INTOtx_test select 6;

从Next-Key Lock的定义可以看出,会话A锁定范围(3,5],会话B插入6不在会话A的范围内,理论上会话B应该可以提交,不用等待会话A释放锁,然而事实上会话B还是会发生阻塞,这又是为什么?原因如下:

InnoDB存储引擎会对辅助索引下一个键值加上GapLock,下一个键为7,Gap Lock范围是(5,7)。所以会话A执行SELECT * from tx_test where id=5 FOR UPDATE的真实锁定范围为(3,5]与(5,7),也就是(3,6),因此会话B插入6在锁定范围内,会阻塞等待锁。

 

然后讨论第三种情况:

表数据1,3,5,7,隔离级别REPEATABLE READ,会话A执行SELECT * from tx_test where id=5 FOR UPDATE,然后会话B执行INSERT INTOtx_test select 3;

那么会话B会阻塞等待锁吗?

答案是会的?

这会让人有点困惑,因为结合之前的讨论,会话A执行SELECT * from tx_test where id=5 FOR UPDATE锁定的范围是(3,6),会话B插入3不在该范围内,那么为啥还会阻塞?

其实InnoDB对于insert操作,会检查插入记录的下一条记录是否被锁定,如果下一条记录被锁定了,那么不允许插入。所以这里会话B是在等待会话A释放id=5的锁释放。

 

到这里,我这篇文章想讨论的问题已经说清楚了,但是我在真实测试以上场景的时候发现了一个有趣的现象:

就是当表中数据量很小的时候,会发现锁的范围与预期的不一样。

数据准备:

create table tx_test(id int ,KEY (`id`))ENGINE=InnoDB;

INSERT INTO tx_test select 1;

INSERT INTO tx_test select 3;

INSERT INTO tx_test select 5;

测试:

会话A: BEGIN;

会话A: SELECT * from tx_test where id>3 FOR UPDATE;

会话B:INSERT INTO tx_test select 0;

此时会话B会阻塞。其实按照之前的分析,此处会话A锁住的范围应该是(3,+∞),那么0不在锁的范围内,应该可以插入数据才对。我猜测应该是发生了锁升级,原来的行锁升级为页锁或者表锁,因此导致会话B所等待。

这也只是我的猜测,知道其中原理的人希望可以不吝赐教。