MySQL 30 用动态的观点看加锁

首先复习一下加锁规则:

  • 原则1:加锁的基本单位是next-key lock,是一个前开后闭区间;

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

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

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

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

接下来的讨论基于下表t:

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);

不等号条件里的等值查询

等值查询和遍历有什么区别?为什么当where条件是不等号,这个过程也有等值查询?

sql 复制代码
begin;
select * from t where id>9 and id<12 order by id desc for update;

利用加锁规则,这个语句的加锁范围是主键索引上的(0,5]、(5,10]、(10,15)。id=15没有加上行锁是因为用到了优化2,退化为了间隙锁。

但是查询语句里where条件不是等号,这里的等值查询又是从哪来的呢?

分析索引id的示意图:

  • 由于语义是order by id desc,要拿到满足条件的所有行,优化器必须先找到第一个id<12的值;

  • 该过程需要搜索索引树找到id=12的值,但最终没找到,只找到(10,15)的间隙;

  • 然后向左遍历,该遍历过程不是等值查询,会扫描到id=5这一行,会加一个(0,5]。

也就是说,在执行过程中,通过树搜索方式定位记录时用的是等值查询的方法。

等值查询的过程

下面这个语句的加锁范围是什么呢?

sql 复制代码
begin;
select id from t where c in(5,20,10) lock in share mode;

先看语句的explain结果:

该语句使用了索引c并且rows=3,说明这三个值都是通过B+树搜索定位的。

在查找c=5时,先锁住了(0,5],但因为c不是唯一索引,为了确认还有没有其他c=5的记录,需要向右遍历,直到c=10才确认没有,该过程满足优化2,所以加间隙锁(5,10)。

同样的,执行c=10 的时候,加锁的范围是(5,10]和(10,15);执行c=20的时候,加锁的范围是(15,20]和(20,25)。

这些锁是在执行过程中一个一个加的,而不是一次性加上去的。

假设同时有另外一个语句:

sql 复制代码
select id from t where c in(5,20,10) order by c desc for update;

间隙锁不互锁,但这两条语句都会在索引c上的c=5、10、20三行记录上加记录锁。

由于两条语句要加锁相同的资源,但加锁顺序相反,当这两条语句并发执行的时候,就可能出现死锁。

关于死锁的信息,MySQL只保留了最后一个死锁的现场,但这个现场还是不完备的。接下来就分析上面例子的死锁现场。

怎么看死锁?

出现死锁后,执行show engine innodb status命令能输出很多信息,其中有一节LATESTDETECTED DEADLOCK,就是记录的最后一次死锁信息。

该结果分为三部分:

  • (1) TRANSACTION:是第一个事务的信息;

  • (2) TRANSACTION:是第二个事务的信息;

  • WE ROLL BACK TRANSACTION (1):是最终处理结果,表示回滚了第一个事务。

第一个事务的信息中:

  • WAITING FOR THIS LOCK TO BE GRANTED:表示这个事务在等待的锁信息;

  • index c of table test.t:说明在等的是表t的索引c上面的锁;

  • lock mode S waiting:表示这个语句要自己加一个读锁,当前的状态是等待中;

  • Record lock:说明这是一个记录锁;

  • n_fields 2:表示这个记录是两列,也就是字段c和主键字段id;

  • 0: len 4; hex 0000000a; asc ;;:是第一个字段c。值是十六进制a,也就是10;

  • 1: len 4; hex 0000000a; asc ;;:是第二个字段,也就是主键id,值也是10;

  • 这两行里面的asc表示的是,接下来要打印出值里面的"可打印字符",但10不是可打印字符,因此就显示空格;

  • 第一个事务信息就只显示出了等锁的状态,在等待(c=10,id=10)这一行的锁;

第二个事务的信息中:

  • " HOLDS THE LOCK(S)"用来显示这个事务持有哪些锁;

  • index c of table test.t 表示锁是在表t的索引c上;

  • hex 0000000a和hex 00000014表示这个事务持有c=10和c=20这两个记录锁;

  • WAITING FOR THIS LOCK TO BE GRANTED,表示在等(c=5,id=5)这个记录锁。

从上面这些信息中能知道:

  • lock in share mode这条语句,持有c=5的记录锁,在等c=10的锁;

  • for update这个语句,持有c=20c=10的记录锁,在等c=5的记录锁。

因此导致死锁,由此得到结论:

  • 由于锁是一个个加的,要避免死锁,对同一组资源要按照尽量相同的顺序访问;

  • 在发生死锁的时刻,for update语句占用的资源更多,回滚成本更大,因此InnoDB选择了回滚成本更小的lock in share mode语句来回滚。

怎么看锁等待?

看完死锁,再看一个锁等待的例子。

由于session A并没有锁住c=10,所以session B删除这一行是可以的,但之后再想insert这一行回去就不行了。

此时执行show engine innodb status,锁信息是在TRANSACTIONS这一节:

  • index PRIMARY of table test.t :表示这个语句被锁住是因为表t主键上的某个锁;

  • lock_mode X locks gap before rec insert intention waiting:

    • insert intention:表示当前线程准备插入一个记录,这是一个插入意向锁。可以认为它就是这个插入动作本身;

    • gap before rec:表示这是一个间隙锁,而不是记录锁。这个gap是在哪个记录之前的呢?接下来的0~4这5行的内容就是这个记录的信息;

  • n_fields 5表示这一个记录有5列:

    • 0: len 4; hex 0000000f; asc ;; 第一列是主键id字段,这个间隙是id=15之前的,因为id=10已经不存在了,它表示的就是(5,15);

    • 1: len 6; hex 000000000513; asc ;; 第二列是长度为6字节的事务id,表示最后修改这一行的是trx id为1299的事务;

    • 2: len 7; hex b0000001250134; asc % 4;; 第三列长度为7字节的回滚段信息。acs后面有显示内容 (% 和 4),这是因为刚好这个字节是可打印字符。后面两列是c和d的值,都是15。

由此可知,delete操作删除了id=10的行,原来的间隙(5,10)、(10,15)变成了(5,15)。

有个结论:所谓间隙,其实是由"这个间隙右边的记录"定义的。

update的例子

再看一个update语句的案例:

session A的加锁范围是索引c上的(5,10]、(10,15]、(15,20]、(20,25]、(25,supremum]。

session B第一个语句要把c=5改为c=1,可以理解为两步:

  • 插入(c=1,id=5);

  • 删除(c=5,id=5)。

根据上面的结论,间隙是由间隙右边的记录定义,此时session A加锁范围变为:

session B的第二个语句拆成两步:

  • 插入(c=5,id=5);

  • 删除(c=1,id=5)。

第一步试图在间隙锁(1,10)插入数据,被堵住。

相关推荐
2301_803554527 小时前
mysql(自写)
数据库·mysql
柏油9 小时前
MySQL InnoDB 架构
数据库·后端·mysql
JavaArchJourney11 小时前
MySQL 索引:原理篇
java·后端·mysql
Jasonakeke11 小时前
【重学 MySQL】九十三、MySQL的字符集的修改与底层原理详解
数据库·mysql·adb
老友@13 小时前
MySQL 索引失效全解析与优化指南
数据库·mysql·索引失效·索引
共享家952713 小时前
MySQL-事务(下)-MySQL事务隔离级别与MVCC
数据库·mysql
秋难降14 小时前
零基础学习SQL(十)——性能分析
数据库·sql·mysql
cooldream200914 小时前
centos7中MySQL 5.7.32 到 5.7.44 升级指南:基于官方二进制包的原地替换式升级
数据库·mysql
百锦再16 小时前
SQLSugar 封装原理详解:从架构到核心模块的底层实现
sql·mysql·sqlserver·架构·core·sqlsugar·net