默认情况下,InnoDB在REPEATABLE READ事务隔离级别下运行。在这种情况下,InnoDB使用next-key锁进行搜索和索引扫描,这可以防止幻行
索引锁的类型:
Record Locks:行锁,对一行记录进行加锁Gap Locks:间隙锁,对范围记录进行加锁Next-Key Locks:Record Locks + Gap LocksInnoDB所有锁的类型详见innodb-locking,如共享(S)锁和排它(X)锁
如果在同一事务中先查询数据,然后插入或更新相关数据,则常规SELECT语句无法提供足够的保护。因为其他事务可以更新或删除刚刚查询的相同行。
InnoDB支持两种类型的锁定读取,可提供额外的安全性
当事务提交或回滚时,所有由FOR SHARE和FOR UPDATE查询设置的锁都会被释放。
只有在禁用自动提交时才能锁定读取(通过使用START TRANSACTION或设置autocommit为
0开始事务)。
SELECT ... FOR SHARE会在扫描到的行里设置共享锁。其他会话可以读取这些行,但在您的事务提交之前不能修改它们。如果其中任何行被另一个尚未提交的事务更改,您的查询将等待该事务结束,然后使用最新值。
FOR SHARE只会锁定扫描过程中使用的索引里的记录行,即如果你的查询正好使用了覆盖索引,那么只有这个索引里的记录行会被锁定,主键索引的记录行是不会被锁定的。FOR SHARE是LOCK IN SHARE MODE的代替版,支持额外的功能,详见 Locking Read Concurrency with NOWAIT and SKIP LOCKED
对于搜索遇到的索引记录,锁定行和任何关联的索引条目,与使用UPDATE语句一致
当事务更新表中的一行或使用SELECT FOR UPDATE锁定时,InnoDB会在该行上建立一个列表或锁定队列。相应已锁定的行或间隙能在innoDB对应的表中查询
MySQL 5.7 使用innodb_lock_waits
MySQL 8.0 使用data_lock_waits
两个原则:
next-key lock,next-key lock是前开后闭区间。两个优化:
next-key lock退化为行锁。next-key lock退化为间隙锁。注意,非等值查询是不会优化的
bug:
<=查询时,会锁住下一个next-key的前开后闭区间(MySQL 8.0.17修复,修改为前开后开区间)示例的建表语句及初始化语句如下
CREATE TABLE `t` (
`id` int(11) NOT NULL,
`c` int(11) DEFAULT NULL,
`d` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `c` (`c`)
) ENGINE=InnoDB;
insert into t values(0,0,0),(5,5,5),
(10,10,10),(15,15,15),(20,20,20),(25,25,25);
如上一共插入6条数据,对应next-key lock:(-∞,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20,25]、(25,+∞),由于
next-key lock包含行锁,因此会形成前开后闭区间范围
| session A | session B |
|---|---|
| begin; select * from t where id = 5 for update; | |
| insert into t values(3, 3, 3); pass | |
| update t set c = c + 1 where id = 5; blocked |
最终加锁范围id=5行锁
| session A | session B |
|---|---|
| begin; select * from t where id = 7 for update; | |
| insert into t values(8, 8, 8); blocked | |
| update t set c = c + 1 where id = 10; pass |
最终加锁范围(5,10)
| session A | session B |
|---|---|
| begin; select id from t where c = 5 for share; | |
| update t set d=d+1 where id=5; pass | |
| insert into t values(8, 8, 8); blocked |
最终加锁范围(0,10)
需要注意,在这个例子中,for share只锁覆盖索引,但是如果是 for update 就不一样了。 执行 for update 时,系统会认为你接下来要更新数据,因此会顺便给主键索引上满足条件的行加上行锁。
| session A | session B |
|---|---|
| begin; select * from t where id>10 and id<=15 for update; | |
| insert into t values(16, 16, 16); blocked | |
| update t set c = c + 1 where id = 20; pass |
最终加锁范围(10,20]
<=查询时,会锁住下一个next-key的前开后闭区间(15,20]| session A | session B |
|---|---|
| begin; select * from t where c>=10 and c<11 for update; | |
| insert into t values(8, 8, 8); blocked | |
| update t set d = d + 1 where c = 15; blocked |
最终加锁范围(5,15]
接下来的例子,是为了更好地说明“间隙”这个概念。这里,我给表 t 插入一条新记录。
mysql> insert into t values(30,10,30);
| session A | session B |
|---|---|
| begin; delete from t where c = 10; | |
| insert into t values(6, 5, 6); blocked | |
| insert into t values(4, 5, 6); pass | |
| update t set d = d + 1 where c = 15; pass |
最终加锁范围(c=5,id=5)~(c=15,id=15)开区间。(c=5,id=5)和(c=15,id=15)这两行上都没有锁

辅助索引的叶子节点中得数据是顺序存放的
与非唯一索引间隙范围中的例子为对照案例,场景如下所示:
| session A | session B |
|---|---|
| begin; delete from t where c=10 limit 2; | |
| insert into t values(12, 12, 12); pass |
最终加锁范围(c=5,id=5)~(c=10,id=30)前开后闭区间

这个例子对我们实践的指导意义就是,在删除数据的时候尽量加 limit。这样不仅可以控制删除数据的条数,让操作更安全,还可以减小加锁的范围。
如果加了关键字desc,优化规则依然有效,依旧是前开后闭,向右扫描变成了向左扫描
| session A | session B |
|---|---|
| begin; select * from t where c>=10 and c<=15 order by c desc for update; | |
| insert into t values(8, 8, 8); blocked | |
| update t set d = d + 1 where c = 15; blocked |
索引向左扫描,最终索引c加锁范围(0,15),主键索引上id=10/15两个行锁
前面的例子中,我们在分析的时候,是按照 next-key lock 的逻辑来分析的,因为这样分析比较方便。最后我们再看一个案例,目的是说明:next-key lock 实际上是间隙锁和行锁加起来的结果。
| session A | session B |
|---|---|
| begin; select id from t where c=10 for share; | |
| update t set d=d+1 where c=10; blocked | |
| insert into t values(8, 8, 8); | |
| ERROR 1213(40001):Deadlock found when trying to get lock; try restarting transaction |
现在,我们按时间顺序来分析一下为什么是这样的结果。
你可能会问,session B 的 next-key lock 不是还没申请成功吗?
其实是这样的,session B 的“加 next-key lock(5,10] ”操作,实际上分成了两步,先是加 (5,10) 的间隙锁,加锁成功;然后加 c=10 的行锁,这时候才被锁住的。
我们在分析加锁规则的时候可以用 next-key lock 来分析。但是要知道,具体执行的时候,是要分成间隙锁和行锁两段来执行的。
参考资料: