MySQL 21 为什么我只改一行的语句,锁这么多?

上篇文章中,介绍了间隙锁和临键锁,但并未说明加锁规则。本文首先介绍加锁规则,由于间隙锁在可重复读隔离级别下才有效,因此接下来的内容默认在可重复读隔离级别下。

加锁规则(限5.x系列<=5.7.24, 8.0系列<=8.0.13):

  • 原则1:加锁的基本单位是临键锁,是一个前开后闭区间;

  • 原则2:查找过程中访问到的对象才会加锁;

  • 优化1:索引上的等值查询,给唯一索引加锁的时候,临键锁退化为行锁;

  • 优化2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,临键锁退化为间隙锁;

  • 一个bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止。

后续例子用到的表:

sql 复制代码
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);

案例一:等值查询间隙锁

由于表中没有id=7的记录,用加锁规则判断:

  • 根据原则1,加锁单位是临键锁,session A加锁范围是(5,10];

  • 根据优化2,这是一个等值查询id=7,而id=10不满足查询条件,临键锁退化成间隙锁,因此最终加锁范围是(5,10)。

所以session B要插入id=8的记录会被锁住,但是session C修改id=10这行是可以的。

案例二:非唯一索引等值锁

这里session A要给索引c上c=5这一行加读锁:

  • 根据原则1,加锁单位是临键锁,因此会给(0,5]加临键锁;

  • 由于c是普通索引,因此仅访问c=5这一条记录不能马上停下来,需要向右遍历查到c=10才放弃。根据原则2,访问到的都要加锁,因此给(5,10]加临键锁;

  • 这个遍历符合优化2,由于最后一个值不满足c=5这个等值条件,临键锁退化成间隙锁;

  • 根据原则2,只有访问到的对象才会加锁,这个查询使用覆盖索引,不需要访问主键索引,所以主键索引上不加任何锁,因此session B的update语句可以成功。

而session C的插入操作,会被session A的间隙锁(5,10)锁住。

在该案例中,lock in share mode只锁覆盖索引,但如果是for update就不同了,因为系统会认为接下来更新数据,会顺便给主键索引上满足条件的行加上行锁。

该案例说明,锁是加在索引上的,同时如果要用lock in share mode来给行加读锁避免数据被更新,就必须绕过覆盖索引的优化,在查询字段中加入索引中不存在的字段,比如将session A的查询语句改成select d from t where c=5 lock in share mode

案例三:主键索引范围锁

考虑下面这两条查询语句,加锁范围是否相同:

sql 复制代码
select * from t where id=10 for update;
select * from t where id>=10 and id<11 for update;

在逻辑上,这两条查询语句等价,但加锁规则不太一样。看看第二个语句的加锁效果:

分析session A的加锁情况:

  • 先找到第一个id=10的行,本该加临键锁(5,10],根据优化1,主键是唯一索引,因此该临键锁退化成行锁,只加了id=10这一行的行锁;

  • 范围查找会继续往后找,找到id=15这一行停下来,因此会加临键锁(10,15]。

这里需要注意的是,session A定位查找id=10的行的时候,是当做等值查询来判断的,而向右扫描到id=15的时候,用的是范围查询判断。

案例四:非唯一索引范围锁

由于索引c是非唯一索引,与案例三相比,没有优化规则,因此最终session A加的锁是:索引c上的(5,10]和(10,15]这两个临键锁。

案例五:唯一索引范围锁bug

session A是一个范围查询,按照原则1的话,应该是索引id上只加(10,15]这个临键锁,且由于id唯一,所以循环判断到id=15这一行就应该停止。

但是实现上,InnoDB会往前扫描到第一个不满足条件的行为止,即id=20,由于这是范围扫描,因此索引id上的(15,20]这个临键锁也会被锁上。

所以session B和session C的操作都会被锁住。

案例六:非唯一索引上存在等值的例子

接下来的例子,是为了更好说明间隙的概念。这里插入一条新纪录:

sql 复制代码
insert into t values(30,10,30);

新插入一行后,表里有两个c=10的行。由于非唯一索引上包含主键的值,所以不存在完全相同的两行,此时索引c:

索引c中两个c=10的记录之间,也是有间隙的。

接下来看例子:

session A在遍历时,先访问第一个c=10的记录,根据原则1,会加(c=5,id=5)到(c=10,id=10)的临键锁。之后继续向右查找,直到碰到(c=15,id=15)这一行,根据优化2,这是一个等值查询,向右查找到了不满足条件的行,会退化成(c=10,id=10)到(c=15,id=15)的间隙锁。

因此delete语句的加锁范围实际上如下:

虚线表示这是个开区间。

案例七:limit语句加锁

案例六的对照案例:

表t里c=10的记录只有两条,因此limit 2不影响删除效果,但会影响加锁效果。可以看到session B的插入语句通过,跟案例六结果不同。

这是因为加了limit 2后,遍历到(c=10,id=30)这一行后,满足条件的语句已经有两条,循环结束。

因此在该案例中,加锁范围如下:

该案例的指导意义就是,在删除数据的时候尽量加上limit。

案例八:一个死锁的例子

该案例目的是说明:临键锁实际上是间隙锁和行锁加起来的结果。

按顺序分析:

  • session A启动事务后,在索引c上加了(5,10]和(10,15)的锁;

  • session B的update语句要在索引c上加(5,10],进入锁等待;

  • session A要插入时被session B的间隙锁锁住。由于出现死锁,InnoDB会让session B回滚。

可能会有疑惑,session B的临键锁还没申请成功,为什么也会死锁?

因为session B的临键锁实际分为两步,先加(5,10)的间隙锁,加锁成功,然后加c=10的行锁才进入等待。

相关推荐
五阿哥永琪35 分钟前
MySQL 慢查询定位与 SQL 性能优化实战指南
sql·mysql·性能优化
xiaok4 小时前
GROUP BY进阶用法
mysql
李慕婉学姐4 小时前
【开题答辩过程】以《基于Android的健康助手APP的设计与实现》为例,不知道这个选题怎么做的,不知道这个选题怎么开题答辩的可以进来看看
android·java·mysql
qq_12498707534 小时前
基于springboot健康养老APP的设计与实现(源码+论文+部署+安装)
java·spring boot·后端·mysql·微信小程序·毕业设计
亚林瓜子5 小时前
mysql命令行手动导入csv数据到指定表
数据库·mysql·gui·csv·cli·db·import
一分半心动5 小时前
lnmp架构 mysql数据库Cannot assign requested address报错解决
linux·mysql·php
ChristXlx5 小时前
Linux安装mysql(虚拟机适用)
linux·mysql
瀚高PG实验室6 小时前
timestampdiff (MYSQL)函数在Highgo DB中的写法
数据库·mysql·瀚高数据库
还是鼠鼠7 小时前
SQL语句执行很慢,如何分析呢?
java·数据库·mysql·面试
云和数据.ChenGuang7 小时前
批量给100台服务器装系统,还要完成后续的配置和软件部署
运维·服务器·开发语言·mysql