说到“锁升级”机制,大家都会想到上篇博客中写到的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备注
1SET SESSION tx_isolation='READ-COMMITTED' 设置隔离级别为读已提交
2BEGIN; A开启事务
3SELECT * 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
1BEGIN;
2SELECT * 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
1BEGIN;
2SELECT * FROM t WHERE a=5 FOR UPDATE
3 BEGIN;
4 INSERT INTO t SELECT 4
5 COMMIT; (成功 不被阻塞)
6COMMIT;

在这种情况下,我们发现无论我们如何在表中插入什么数据,都不会影响查询a=5这一行的结果。因为a这一列是唯一索引无论怎么插入都不会影响到对唯一索引的再一次查询结果

因此在这种情况下,也就是在查询的列是唯一索引的情况下,InnoDB会将Next-Key Lock算法降级为Record Lock,从而提高并发性

这就是非常简单的MySQL中的“锁降级”机制了,除开前面铺垫的部分来看内容相当简单呢。写这篇文章只是之前看到JDK有锁升级的机制,然后又碰到了一个锁降级,觉得十分有意思,因此记录了下来~

Last modification:July 17th, 2021 at 10:49 pm