写点什么

MySQL- 技术专题 - 锁的介绍分析

发布于: 2021 年 04 月 08 日
MySQL-技术专题-锁的介绍分析

前提概述

MySQL 大概率上都会遇到死锁问题,这实在是个令人非常头痛的问题。本文将会对死锁进行相应介绍,对常见的死锁案例进行相关分析与探讨,以及如何去尽可能避免死锁给出一些建议。

什么是死锁

死锁是并发系统中常见的问题,同样也会出现在数据库 MySQL 的并发读写请求场景中。当两个及以上的事务,双方都在等待对方释放已经持有的锁因为加锁顺序不一致造成循环等待锁资源,就会出现“死锁”。常见的报错信息为 ” Deadlock found when trying to get lock... ”。

举例来说 A 事务持有 X1 锁 ,申请 X2 锁,B 事务持有 X2 锁,申请 X1 锁。A 和 B 事务持有锁并且申请对方持有的锁进入循环等待,就造成了死锁。

如上图,是右侧的四辆汽车资源请求产生了回路现象,即死循环,导致了死锁。

从死锁的定义来看,MySQL 出现死锁的几个要素为:

  • 两个或者两个以上事务

  • 每个事务都已经持有锁并且申请新的锁

  • 锁资源同时只能被同一个事务持有或者不兼容

  • 事务之间因为持有锁和申请锁导致彼此循环等待

怎么避免死锁呢?

  • 将大事务拆成小事务

  • 添加合理的索引,走索引避免为每一行加锁,降低死锁的概率

  • 避免业务上的循环等待(如加分布式锁之类的)

  • 降低事务隔离级别(如 RR -> RC 当然不建议这么干)

  • 并发插入时使用 replace/on duplicate 也可以避免死锁


说明:后续内容实验环境为 5.7 版本,默认隔离级别为 RR(可重复读)

innoDB 锁类型

为了分析死锁,我们有必要对 InnoDB 的锁类型有一个了解。

MySQL InnoDB 引擎实现了标准的行级别锁:共享锁(S lock) 和排他锁 (X lock),此外为了解决"幻读"问题,引入了 gap lock 以及 next key lock,来了优化以及减少加锁的成本引入了一种意向锁的,比如插入意向锁


本文将主要介绍的以下几种锁

  • 行锁(record lock): 注意它针对索引的锁(如果没有索引时,最终行锁就会导致整个表都会被锁住

  • 共享锁(S Lock): 也叫读锁,共享锁之间不会相互阻塞

  • 排它锁(X Lock): 也叫写锁,排它锁一次只能有一个 session(或者说事务?)持有

  • 间隙锁(gap lock): 针对索引之间的间隙

  • Next-key 锁(Next-key lock):可以简单理解为行锁 + 间隙锁


共享锁与排他锁区分

行锁|表锁|gap 锁|next-key 锁

  • 没有索引,加 S/X 锁最终都是锁整表 (为啥?因为锁是针对索引而言的

  • 根据主键/唯一键锁定确定的记录:行锁

  • 普通索引或者范围查询:gap lock / next key lock

行锁和 gap 锁之间最大的区别

  • 行锁针对确定的记录

  • 间隙锁是两个确定记录之间的范围;

  • next key lock 则是除了间隙还包括确定的记录

实例演示

上面的两个说明,自然就想在实际的 case 中操刀分析一下,不同的 sql 会产生什么样的锁效果

  • 针对表中一条确定的记录加 X 锁,是只有行锁嘛?

  • 针对表中多条确定的记录加 X 锁,又会怎样?

  • 针对表中一条不存在的记录加 X 锁,会有锁产生吗?如果是 gap 锁,那区间怎么定?

  • 针对范围加 X 锁,产生的 gap 锁范围怎么确定呢?


分析上面几种 case 之前,我们得先有一个概念,锁是针对索引而言的,这一点非常非常重要

不同的索引,我们需要分别进行测试(其实就是唯一索引与普通索引)。


  • 无索引表 TN

  • 唯一索引表 TU

  • 普通索引表 TI

精确匹配

即我们的 sql 可以精确命中某条记录时,锁的情况如下:

请注意上面的结论,无索引时锁全表好理解,但是普通索引的 TI 表,居然还有一个[10, 30)的 gap 锁就有点超乎我们的想象了;

  • gap lock: 范围为[10, 30)

  • 因此无法插入 uid=[10,30)

  • 注意,uid=10 上有 gap 只是不能插入记录但是加 X 锁是没有问题


精确查询未匹配

当我们锁的记录不存在时,锁情况如下:

从上面的测试也可以看出,uid=30 没有被锁住,这里只在 uid=(20, 30)这一区间添加了 gap 锁

唯一索引与普通索引表现一致,会阻塞 insert 的插入意向锁


范围查询

当我们锁一段区间时,锁的情况如下:

简单来说,范围查询时,添加 next key lock,根据我们的查询条件,找到最左边和最右边的记录区间

如 uid > 15 and uid < 25,找到的记录是(1, 10), (10, 30)。


  • gap 锁为(10, 30)

  • next key lock 会为右边添加行锁,即 uid=30 加 X 锁

  • 因此针对 uid=30 记录加锁会被阻塞(但是针对 uid=28,29 加 x 锁则不会被阻塞)


说明:范围加 x 锁时,可能锁住不在这个区间的记录,一不小心可能导致死锁哦


在 RR 隔离级别中,我们一般认为可以产生锁的语句为:

  • SELECT ... FOR UPDATE: X 锁

  • SELECT ... LOCK IN SHARE MODE: S 锁

  • update/delete: X 锁


| 普通索引 | 精确匹配且命中 | 行锁 + gap lock (上一个记录和下个记录区间,左闭右开,左边记录非行锁)

| 普通索引 | 精确匹配,未命中 | gap lock |

| 普通索引 | 范围查询 | next key lock |





  • 同事务可以同时对同一行记录加 S 锁。

  • 如果一个事务对某一行记录加 X 锁,其他事务就不能加 S 锁或者 X 锁,从而导致锁等待。

事务 T1 持有行 r 的 S 锁,那么另一个事务 T2 请求 r 的锁时,会做如下处理:

  • T2 请求 S 锁立即被允许,结果 T1 T2 都持有 r 行的 S 锁

  • T2 请求 X 锁不能被立即允许

如果 T1 持有 r 的 X 锁,那么 T2 请求 r 的 X、S 锁都不能被立即允许,T2 必须等待 T1 释放 X 锁才可以,因为 X 锁与任何的锁都不兼容。共享锁和排他锁的兼容性如下所示:

间隙锁(gap-lock)

间隙锁锁住一个间隙以防止插入。假设索引列有 2, 4, 8 三个值,如果对 4 加锁,那么也会同时对(2,4)和(4,8)这两个间隙加锁。其他事务无法插入索引值在这两个间隙之间的记录。但是,间隙锁有个例外:

  • 如果索引列是唯一索引,那么只会锁住这条记录(只加行锁),而不会锁住间隙

  • 对于联合索引且是唯一索引,如果 where 条件只包括联合索引的一部分,那么依然会加间隙锁

next-key lock

next-key lock 实际上就是: 行锁+这条记录前面的 gap lock 的组合。假设有索引值 10,11,13 和 20,那么可能的 next-key lock 包括:

(负无穷,10]

(10,11]

(11,13]

(13,20]

(20,正无穷)


RR 隔离级别下,InnoDB 使用 next-key lock 主要是防止幻读问题产生。

普通索引下添加 x 锁,居然会加一个 gap 锁,而且这个 gap 区间是前一个记录(并包含它),到下一个记录,如 uid = 20, 前后两个记录为(1, 10), (10, 30)


意向锁(intention lock )

InnoDB 为了支持多粒度的加锁,允许行锁和表锁同时存在。为了支持在不同粒度上的加锁操作,InnoDB 支持了额外的一种锁方式,称之为意向锁( Intention Lock )。意向锁是将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度上进行加锁

意向锁分为两种:

  • 意向共享锁( IS ):事务有意向对表中的某些行加共享锁

  • 意向排他锁( IX ):事务有意向对表中的某些行加排他锁

由于 InnoDB 存储引擎支持的是行级别的锁,因此意向锁其实不会阻塞除全表扫描以外的任何请求。表级意向锁与行级锁的兼容性如下所示:

插入意向锁(Insert Intention lock)

插入意向锁是在插入一行记录操作之前设置的一种间隙锁,插入意向锁其实是一种特殊的 gap lock,但是它不会阻塞其他锁,这个锁释放了一种插入方式的信号,即多个事务在相同的索引间隙插入时如果不是插入间隙中相同的位置就不需要互相等待。


假设某列有索引值 2,6,只要两个事务插入位置不同(如事务 A 插入 3,事务 B 插入 4),那么就可以同时插入

它的属性

  • 它不会阻塞其他任何锁;

  • 它本身仅会被 gap lock 阻塞

重要知识点

  • 通常 insert 语句,加的是行锁,排它锁。

  • 在 insert 之前,先通过插入意向锁,判断是否可以插入(仅会被 gap lock 阻塞)。

  • 当插入唯一冲突时,在重复索引上添加读锁。

原因如下
  • 事务 1 插入成功未提交,获取了排它锁,但是事务 1 最终可能会回滚,所以其他重复插入事务不应该直接失败,这个时候他们改为申请读锁。

锁模式兼容矩阵

横向是已持有锁,纵向是正在请求的锁:


针对上面的矩阵,理解下面几个原则即可推导上面矩阵

  • gap lock 只会与插入意向锁冲突

  • X 行锁会与行锁冲突

  • next key lock: 行锁 + gap 锁 锁区间内,插入冲突; 行锁的 X 锁冲突


发布于: 2021 年 04 月 08 日阅读数: 73
用户头像

我们始于迷惘,终于更高水平的迷惘。 2020.03.25 加入

🏆 【酷爱计算机技术、醉心开发编程、喜爱健身运动、热衷悬疑推理的”极客狂人“】 🏅 【Java技术领域,MySQL技术领域,APM全链路追踪技术及微服务、分布式方向的技术体系等】 🤝未来我们希望可以共同进步🤝

评论

发布
暂无评论
MySQL-技术专题-锁的介绍分析