说到“锁升级”机制,大家都会想到上篇博客中写到的JDK锁优化里面的偏向锁、轻量级锁那一套锁优化机制。然鹅在MySQL当中也存在一种"锁降级"的机制……
本篇文章参考于《MySQL技术内幕——InnoDB存储引擎》
1. MySQL中的锁算法
在并发访问的控制中,锁充当了重要的角色。要最大程度地利用数据库的并发访问,并且确保每个用户能以一致的方式读取和修改数据库,就必须在锁的粒度等方法多下功夫。InnoDB
存储引擎与MySQL
中其他的引擎相比在这一方面可以说是技高一筹,其中有一个可能没什么存在感的机制——锁降级机制在某些情况也会起到优化的作用
说到“锁降级”,那么首先得看一下MySQL中(具体说是InnoDB引擎中)有哪些锁的算法吧
1.1 Record Lock 记录锁
顾名思义,Record Lock
锁住的是索引记录,即给单个行记录
上的锁。这也是区别于MyISAM
引擎的一点,因为后者锁粒度只提供到了表级锁
。
1.2 Gap Lock 间隙锁
间隙锁锁住的是一个范围,这个范围不包括记录本身
例如一个索引有10、20、30、50这四个值,那么间隙锁可以有5个锁定区间,分别为
(-∞, 10) (10, 20) (20, 30) (30, 50) (50, +∞)
1.3 Next-Key Lock
Next-Key Lock
可以理解为Gap Lock
+ Record Lock
,它锁定的也是一个范围,并且包括记录本身
在Next-Key Lock算法下,InnoDB对于行查询都是采用这种锁定算法。例如对上面的例子来说,那么采用这种锁的区间为
(-∞, 10] (10, 20] (20, 30] (30, 50] (50, +∞)
Next-key Lock的设计目的是为了解决”幻读“问题,接下来我们看幻读是什么
2. 幻读问题
在MySQL中,采取的默认隔离级别是REPEATABLE READ
,也就是可重复读
。在这个级别下原本会存在幻读的问题,但是InnoDB采用Next-Key Lock
来避免了这个问题。而其他的数据库例如Oracle,需要将隔离级别设置成串行化(SERIALIZABLE
)才能解决幻读问题。
2.1 什么是幻读
幻读是值在同一事务之下,连续执行两次同样的SQL语句可能导致不同的结果,第二次的SQL语句可能会返回之前不存在的行。
区别于不可重复读来说,幻读问题关注的是行的变化,而不可重复读关注的是读到的数据变化。下面看一个幻读演示的例子:
首先先执行SQL来创建一个表,包含1,2,5三个值
CREATE TABLE t (a INT PRIMARY KEY);
INSERT INTO t SELECT 1;
INSERT INTO t SELECT 2;
INSERT INTO t SELECT 5;
时间 | 会话A | 会话B | 备注 |
---|---|---|---|
1 | SET SESSION tx_isolation='READ-COMMITTED' | 设置隔离级别为读已提交 | |
2 | BEGIN; | A开启事务 | |
3 | SELECT * FROM t WHERE a>2 FOR UPDATE (结果:a:4) | 查询并上排它锁 | |
4 | BEGIN; | B开启事务 | |
5 | INSERT INTO t SELECT 4; | 会话B插入一条数据 | |
6 | COMMIT; | B提交事务 | |
7 | 再次执行 SELECT * FROM t WHERE a>2 FOR UPDATE (结果:a:4 , a:5) | 查询到了两个结果 |
在上面的例子中,会话A在一次事务中,第二次查询比第一次多出了一个结果,也就是会话A被会话B的事务给影响到了。这违反了事务的隔离性(Isolation)
2.2 解决幻读问题
InnoDB引擎采用Next-Key Locking
算法避免幻读。对于上面的SQL语句SELECT * FROM t WHERE a>2 FOR UPDATE
,InnoDB会对(2, +∞)这个区间加上排它锁,因此对于这个范围的插入都是不被允许的,从而避免幻读问题。
例如下面的例子, 首先新建一张表z
CREATE TABLE z (a INT, b INT, PRIMARY KEY(a), KEY(b) );
INSERT INTO z SELECT 1,1;
INSERT INTO z SELECT 3,1;
INSERT INTO z SELECT 5,3;
INSERT INTO z SELECT 7,6;
INSERT INTO z SELECT 10,8;
时间 | 会话A | 会话B |
---|---|---|
1 | BEGIN; | |
2 | SELECT * FROM z WHERE b=3 FOR UPDATE | |
3 | BEGIN; | |
4 | INSERT INTO z SELECT 4,2 (阻塞) |
在这个示例中,会话B想要插入一条数据记录,但是被阻塞了,因此此时会话A再次执行同样的查询SQL语句得到的仍然是同样的结果
当会话A执行完SELECT语句之后,InnoDB会将这些区间进行锁定:(1, 3](Next-Key Lock
)以及(3, 6)(Gap Lock
)
除了上面的SQL之外,会话B执行下面的SQL也都会被阻塞
INSERT INTO z SELECT 6,5;
#或
SELECT * FROM z WHERE a=5 LOCK IN SHARE MODE;
3. 锁降级
在铺垫了锁算法以及为什么需要锁算法之后,就可以来分析在什么情况下可以执行“锁降级”操作,并以此提高应用的并发能力了
对于2.1中的表t
CREATE TABLE t (a INT PRIMARY KEY);
INSERT INTO t SELECT 1;
INSERT INTO t SELECT 2;
INSERT INTO t SELECT 5;
我们执行如下SQL,可以发现不会被阻塞
时间 | 会话A | 会话B |
---|---|---|
1 | BEGIN; | |
2 | SELECT * FROM t WHERE a=5 FOR UPDATE | |
3 | BEGIN; | |
4 | INSERT INTO t SELECT 4 | |
5 | COMMIT; (成功 不被阻塞) | |
6 | COMMIT; |
在这种情况下,我们发现无论我们如何在表中插入什么数据,都不会影响查询a=5这一行的结果。因为a这一列是唯一索引,无论怎么插入都不会影响到对唯一索引的再一次查询结果。
因此在这种情况下,也就是在查询的列是唯一索引的情况下,InnoDB会将Next-Key Lock算法降级为Record Lock,从而提高并发性。
这就是非常简单的MySQL中的“锁降级”机制了,除开前面铺垫的部分来看内容相当简单呢。写这篇文章只是之前看到JDK有锁升级的机制,然后又碰到了一个锁降级,觉得十分有意思,因此记录了下来~