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;
然后按下图顺序执行两个事务:
那么,在两种隔离级别下分别会出现什么结果呢?下面细细道来。
在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所等待。
这也只是我的猜测,知道其中原理的人希望可以不吝赐教。