InnoDB:Lock Manager
承接 Skywalker:InnoDB:lock manager (1),这一篇我们研究不同 SQL 对应的加锁方式
MySQL 的默认隔离级别是 Reaptable Read(可重复读),一般来讲 Reaptable Read 的定义是不需要阻止幻读的,但 MySQL 的实现上消除了幻读
幻读(Phantom)
A phantom read occurs when, in the course of a transaction, new rows are added or removed by another transaction to the records being read.
从概念上来讲,只要事务发现一个之前(事务开启后)未见过的数据行,就称作幻读。这里的 "发现" 很有意味,如果事务的执行结果被该数据行所影响,就称之为发现了该数据行
例如:两个事务 T1 / T2,串行执行时 T1 的执行结果,和并行执行时 T1 的执行结果并不相同;而且是由于幻读导致(T1 读到 T2 新插入的数据行)
a write in one transaction changes the result of a search query in another transaction, is called a phantom
从行为上来看,事务发现一个之前未见过的数据行,就是因为事务的 "search query" 的结果被另一个事务改变,则该事务就(可能)会出现幻读。注意,这里的 search query 很广泛,不单指 select 语句,比如 update ... where ...,where 子句也可以叫做 search query(因为也是要查询出来再做修改)。我们看一个存在幻读现象的例子(改变部门 A 的员工工资):
create table t (id int, salary int, department varchar(32), primary key(id), key(deptment));
insert into t values (1,50,'A'),(2,10,'A'),(3,20,'B')
T1 | T2 |
---|---|
update t set salary=salary+20 where department='A' | |
insert into t values (4,5,'A') | |
update t set salary=salary-10 where department='A' |
事务 T1 首先把部门 A 员工工资都 +20,随后又都 -10。如果无法阻挡 T2 的 insert,那么 T1 的第二次 update 最终会导致 id=4 的员工工资为负数(-5)。究其原因是因为 T1 的第二次 update 读到了 T2 新产生的 id=4 的数据行(幻读)
注:在 Reaptable Read 隔离级别下,MySQL 采用 MVCC 的方式来处理只读语句(e.g. select ... from ...),无论该语句执行多少次均会看到相同的结果,因此不会出现幻读的现象
消除幻读
想要消除上面的例子中的幻读,一个朴素的想法就是锁住 department 的全部成员,延伸一下就是锁住 where 子句(或叫做 "谓词") 。
不同于 record lock,这种叫 predicate lock(谓词锁),试图锁住一个范围。当然,在申请 predicate lock 时需要检查是否和已有的 predicate lock 冲突,但不幸的是,这是一个 NP-complete 难题。
我们可以简化这个问题,即放大 lock 的范围。比如看这个 SQL:查找 1 班成绩在 80 和 90 分之间的学生
SELECT name FROM students WHERE class = 1 AND score > 80 AND score < 90 FOR UPDATE;
三种不同范围 Lock 的兼容性
LOCK_GAP 只锁住一个区间、表示不希望其他任何事务在这个区间插入新的记录。因此如果两个 lock 都是 LOCK_GAP 类型,一般是不冲突的(即你不希望区间内插入新纪录,我也不希望区间内插入新纪录)
同时,LOCK_GAP 和 LOCK_REC_NOT_GAP 也是不冲突的,因此 LOCK_GAP 和 LOCK_ORDINARY 依然不冲突
那么 LOCK_GAP 和什么冲突呢?只和插入的操作冲突
lock_rec_insert_check_and_lock {
// insert 操作依然需要申请 LOCK_GAP,我们举个例子
// create table t (id int, primary key(id));
// insert into t values (2)(4)(6)(8);
// 那么对于 insert into t values (5); 会把 cursor 定位于第一个小于 5
// 的记录上(记录 6)。申请区间(4,6)的 LOCK_GAP,同时这个 lock 的 type_mode
// 具有 LOCK_INSERT_INTENTION 标识。这样如果(4,6)区间存在其他的 LOCK_GAP
// 或 LOCK_ORDINARY,则因为 LOCK_INSERT_INTENTION 标识而会产生冲突
const ulint type_mode = LOCK_X | LOCK_GAP | LOCK_INSERT_INTENTION;
const lock_t* wait_for = lock_rec_other_has_conflicting(
type_mode, block, heap_no, trx);
}