> 文档中心 > 一文掌握MySQL中的锁

一文掌握MySQL中的锁

MySQL中锁的详细介绍

  • 解决并发事务带来的问题
    • 读-读情况
    • 写-写情况
    • 读-写或写-读情况
  • 关于快照读和当前读的解释
    • 快照读
    • 当前读
  • MySQL中的行锁和表锁
    • 其他存储引擎的锁
    • Innodb存储引擎中的锁
      • 表级锁
        • 级别的S锁,X锁
        • 表级别的IS锁(意向共享锁),IX锁(意向独占锁)
        • 表级别的AUTO_INC锁(自增锁)
      • 行级锁
        • 记录锁(Record Lock)
        • 间隙锁(Gap Locks)
        • 临键锁(Next-Key Lock)
        • 插入意向锁(Insert Intention Lock)
        • 隐式锁
  • (重点)语句加锁分析:帮助你更加了解MySQL是如何加锁工作的
    • 普通的Select语句
    • 锁定读的语句
      • 写操作加锁分析
        • delete
        • update
          • 未更新主键且修改列前后所占的存储空间不变
          • 未更新主键,但修改列前后所占的存储空间发生变化
          • 更新主键的update操作
        • insert
      • 锁定读的分类
      • (重头戏)锁定读的加锁流程
    • 半一致性读
    • insert
  • 死锁

一篇好的文章带来的是无限的价值!!
如果对文章有什么疑问,欢迎在下方提出,我会在一天内解答!

解决并发事务带来的问题

读-读情况

并发事务同时对一条记录进行读取,本身不会对记录造成什么影响,所以允许这种情况的发生

写-写情况

并发事务同时对同一条记录进行修改的情况, 会造成脏写现象,任何一种隔离级别都不允许它的发生,所以使用加锁的方式,排队等待进行记录的修改。

这个排队的实现,是通过加锁来进行实现的。这个锁结构是在内存中实现的,在事务执行前,锁并不存在。
在这里插入图片描述
当一个事务对记录做改动时,需要先生成一个锁结构,然后在内存中查找是否有与该记录相关联的锁结构,没有的话,则获取锁成功。否则获取锁失败,开始等待,直到其他锁释放,唤醒它。

读-写或写-读情况

在读-写或写-读情况下,会出现脏读,不可重复读,幻读现象。
SQL标准中的四种隔离级别:

隔离级别 脏读 不可重复读 幻读
READ UNCOMMITTED 可能 可能 可能
READ COMMITTED 不可能 可能 可能
REPETABLE READ 不可能 不可能 可能
SERIALIZABLE 不可能 不可能 不可能

每个sql厂商实现的SQL标准都不太一样,MySQL与SQL标准不一样的地方在于:在repeatable read隔离级别下,使用MVCC有可能会出现幻读现象,需要加临键锁解决幻读。(总的来说:在可重复读级别下,普通读采用MVCC,读写可以并发,且很大程度避免幻读。如果在一些特殊情况必须保证不出现幻读,那么则需要加上临键锁保证其不会出现幻读,也就是退化到了可串行化级别。而我们的可串行化,无论是普通读还是加锁读,都是不能并发的,而我们的提交读与可重复读的区别则在于生成ReadView的时机不同而已,我们在上一篇文章中也详细介绍过MVCC的原理)

关于快照读和当前读的解释

快照读

事务利用MVCC进行读取记录的操作叫做快照读,也叫一致性读。所有普通的select在提交读,可重复读隔离级别下,都是属于快照读。快照读对表中的数据不会有任何加锁操作,读写是可以并发的

当前读

在未提交读,可串行化下,普通的select则是当前读。在另外两个隔离级别下,select … for update和lock in share mode读则也是当前读。其他的update和delete在任何隔离级别下则都是当前读

MySQL中的行锁和表锁

其他存储引擎的锁

对于MyISAM,Memeory,这些存储引擎来说,它们只支持表级锁,并且这些存储引擎并不支持事务。这些存储引擎因为其读写不能并发,所以适合使用在只读场景下或者单用户场景下。

Innodb存储引擎中的锁

表级锁

表级别的S锁,X锁

在对表操作alert table,drop table时,其他事务并发对这个表做update,delete ,select操作时会发生阻塞,反过来也是一样的。这个过程其实是在server层使用一种叫做元数据锁实现的。一般不会使用Innodb自己提供的S锁,X锁。

表级别的IS锁(意向共享锁),IX锁(意向独占锁)

  1. 在我们对表加S锁(这里指的是表锁),则需要先检查表里有没有行级别的X锁,有的话,则需要X锁释放后才可以加表级别的S锁
  2. 在我们对表加X锁时,则需要检查表里有没有行级别的S锁和X锁,有的话,也需要等其释放才可以加表级别锁

那我们如何检测表中是否有行级锁呢??一个个检测吗??那肯定不行啊!!死都不会用暴力遍历的。

那我们自然而然就有了意向锁。当加入一个行级别的S锁时,那么首先就需要在表上挂一个IS意向共享锁。当加入一个行级别的X锁时,那么首先也需要在表上挂一个IX意向独占锁。这样加表锁时,就不用遍历啦!!直接看它有没有意向锁就ok

当我们给行加锁时,并不需要看表的意向锁。只有加表锁时,才需要看表的意向锁

表级别的AUTO_INC锁(自增锁)

当我们设置了auto_increament时,就需要我们的AUTO_INC锁了.

系统给auto_increament列进行递增赋值时,实现方式主要有两种:

  1. 采用AUTO_INC锁,一般我们不确定插入的条数时,则是使用这个锁。
    当我们需要插入时,会先给表加一个AUTO_INC锁,将语句全部插入完成后释放
  2. 采用轻量级锁,一般我们插入确定的条数时,则是使用这个锁
    当我们生成了自增列的值过后,就释放该锁,不用等全部的值插入后再进行释放

注意:这里的锁作用范围只是单个语句,并不是提交之后才释放,而是执行完插入就释放。

行级锁

记录锁(Record Lock)

记录锁也就是仅仅把一条记录锁上,官方的类型名称为: LOCK_REC_NOT_GAP 。比如我们把id值为8的那条记录加一个记录锁的示意图如图所示。仅仅是锁住了id值为8的记录,对周围的数据没有影响。
在这里插入图片描述
记录锁分为S锁和X锁

  • 当一个事务获取了一条记录的S型记录锁后,其他事务也可以继续获取该记录的S型记录锁,但不可以继续获取X型记录锁;
  • 当一个事务获取了一条记录的X型记录锁后,其他事务既不可以继续获取该记录的S型记录锁,也不可以继续获取X型记录锁。

间隙锁(Gap Locks)

间隙锁仅仅是为了解决幻读问题而提出的

在这里插入图片描述
如图所示,我们给8加上一个间隙锁,则(3,8)中的间隙其他事务就不能够进行插入了,防止产生幻影记录。那么如何给(20,+无穷)加上间隙锁呢??我们数据页中有两条伪记录:Infimum记录表示页面中最小的记录,Supermum记录表示页面中最大的记录。所以我们只要给Supermum加上间隙锁就可以锁住这一区间了。

临键锁(Next-Key Lock)

就是相当于记录锁+间隙锁的结合,可以锁住当前记录和当前记录前面的间隙

Next-Key Locks是在存储引擎 innodb 、事务级别在 可重复读 的情况下使用的数据库锁,innodb默认的锁就是Next-Key locks。

插入意向锁(Insert Intention Lock)

当一个事务想要执行插入操作时,发现该位置已经有了间隙锁(临键锁也包括),则会生成一个插入意向锁,直到gap释放为止。插入意向锁并不会相互阻塞,相当于多个事务可以同时获取同一个插入意向锁。插入意向锁跟没有一样,并不会阻止其他锁

隐式锁

当我们执行插入操作时,我们是不用加上锁的(前提是该间隙并没有被gap锁住,锁住的话则生成插入意向锁并等待)。

但是!!!
我们考虑这样的情况,我们首先插入一条记录,此时事务并没有提交,且内存中并没有任何的锁结构。那么另一个事务此时执行select … lock in share mode获取它的s锁,那么此时可以获取到s锁,那么不就会产生脏读现象了吗??又或者执行update更新,此时不就会产生脏写现象了吗???

no,no,no!MySQL才不会这么傻呢。为啥叫隐式锁呢?说明锁是隐式的嘛!看下面:

  • 对于聚簇索引来说:当一个进行插入时,该记录的事务id会是当前事务。而当另一个事务尝试读取或修改时,会去看该记录的事务id是否活跃,不活跃说明提交了,可以读取;活跃则说明该事务没有提交,不能读取。此时我们的第二个事务则会帮助它创建一个X锁结构,is_waiting属性为false,自己也创建一个锁结构,is_waiting属性为true,需要等待
  • 对于二级索引来说,并没有事务id,所以也会有不一样:在二级索引页面中,会有一个 最大事务id的属性,记录这修改这个页面的最大事务id。当该事务id小于活跃的最小事务id时,说明已经提交了,则可以读取。如果不小于的话,则需要定位到该记录,然后到聚簇索引里去找,执行上一个的操作

总的来说:当事务插入一个记录时,相当于加了一个隐式锁,如果该事务没有提交,其他事务访问时,则会帮助它生成一个X锁结构,自己也生成一个X锁结构。这样相当于起到了延迟生成锁的作用,减少了开销

(重点)语句加锁分析:帮助你更加了解MySQL是如何加锁工作的

下面才是最重要的内容,它会让我们知道MySQL在不同隔离级别下,执行一些语句,会如何加锁,加什么锁??

普通的Select语句

在不同的隔离级别下,MySQL都会有不同的做法

  • 未提交读(read uncommitted)
    读不加锁,可以直接读取到最新的数据,可能会产生脏读,不可重复读,幻读现象
  • 提交读(read committed)
    读不加锁,使用MVCC,每次普通select时都会生成一个ReadView,这样就避免了脏读现象,但还是会产生不可重复读和幻读现象
  • 不可重复读(repeatable read)
    读不加锁,使用MVCC,只会在第一次普通select时生成ReadView,避免了脏读,不可重复读,很大程度可以避免幻读。为什么是很大程度呢??我们先把串行化讲完再说
  • 串行化(serializable)
    当我们的参数autocommit=0(禁用自动提交),相当于开启了一个事务,我们的普通select便会转换为select… lock in share mode的加锁语句,为其加上s锁
    当我们的参数autocommit=1(开启自动提交),此时一个语句就相当于是一个事务,所以只会读取时生成一个ReadView来进行读取。

回到前面说的MVCC很大程度上可以避免幻读。对于一般情况来说,MVCC确实能解决幻读,但是我们考虑如下一种情况,注意:这里的隔离级别为repeatable read
一文掌握MySQL中的锁
过程分析:事务A首先普通select读取id>14的记录,有四条。此时事务B开始插入数据(事务A是不能阻止插入的),插入了id=19的记录,并且提交。事务A对id=19的记录进行更新(因为update是当前读,所以可以看到该记录,后面会详细讲update),那么此时id=19的记录的事务id就为事务A的了,事务A再次读取时,多读取了19的记录,产生幻读(关于MVCC的详细解答,可以查看我的其他文章)

这里我们可以看到事务A使用两次普通读,产生了幻读现象。由于这种特殊的情况,所以我们说在可重复读隔离级别下MVCC并不能完全避免幻读

锁定读的语句

稍安勿躁,我们先看看来写操作是如何加锁的

写操作加锁分析

delete

我们对记录进行delete操作时,需要先在B+树中找到对应的删除记录,然后在该记录上加上X锁,然后执行delete mark操作。我们可以把这个过程看做是一个加X锁的锁定读

update

未更新主键且修改列前后所占的存储空间不变

当这种情况时,我们也是需要先在B+树中定位到修改的记录,然后加上X锁,修改该记录。我们也可以把这个过程看做是一个加X锁的锁定读

未更新主键,但修改列前后所占的存储空间发生变化

我们也需要先定位到该记录,然后加上X锁,然后将该记录彻底删除,最后再插入一条新记录。我们也可以把这个过程看做是一个加X锁的锁定读

更新主键的update操作

也就是相当于在delete这条记录,然后再insert一条新记录我们也可以把这个过程看做是一个加X锁的锁定读

insert

一般不需要加锁,受到隐式锁保护。也有情况是加插入意向锁,前面也有讲过

锁定读的分类

  1. select … lock in share mode;
  2. select … for update;
  3. update…
  4. delete…

(重头戏)锁定读的加锁流程

首先申明:下面所说的指一般情况,几种特殊情况后面也会讲解。只能说mysql加锁非常复杂,救命!!!真难写

一般情况下:

  1. 首先快速在B+树叶子结点中定位到该条记录,将其作为当前记录
  2. 为当前记录加锁(在未提交读和提交读的隔离级别下,为该记录加记录锁;在不可重复读和可串行化隔离级别下,为该记录加临键锁)
  3. 如果为二级索引,判断索引下推条件是否成立
    索引下推:前面有文章介绍过索引下推的概念,如果在查询中有条件是在索引的记录中的,那么它就直接在存储引擎中判断,这样可以减少回表的次数,提高效率。索引下推只适合二级索引,且只适用于select语句
    如果满足索引下推则直接执行下一步操作。如果不满足索引下推条件,则判断是否符合扫描区间的边界条件,不符合的话直接跳到第六步,向server层返回查询完毕的信息;符合扫描区间的话,则跳到下一条记录,返回步骤2.需要注意的是:步骤3并不会释放锁结构,无论哪种情况
  4. 二级索引的话,执行回表操作
    对回表后的聚簇索引记录加上记录锁。注意:无论是哪种隔离级别,对二级索引回表后的聚簇索引记录都是加记录锁
  5. 判断边界条件是否符合
    如果符合边界条件,则跳到步骤6继续执行。如果不符合边界条件,在未提交读和提交读的隔离级别下,释放加在该记录上的锁;在不可重复读和可串行化隔离级别下,不释放锁。然后向server层返回查询完毕的信息
  6. server层判断搜索的其余条件是否满足
    满足的话,则返回记录给客户端,哪种级别也不释放锁。
    不满足的话,在最低两个级别释放锁,在最高两个级别不释放锁(实在是不想把每个隔离级别都写一遍了,就写最高最低两个吧!!)
  7. 获取下一条记录,跳转回步骤2

具体实例先不写了,读者自己也可以尝试分析一下具体的语句,自己实验一下。
在步骤3中,无论哪种隔离级别,如果不满足索引下推条件的话,都是不会释放锁的。因此我们可以粗略的认为存储引擎层是不配释放锁的,只能在server层释放

上述介绍都是select的情况,那么对于update,delete的情况其实都是差不多的,只不过他们加的是X锁而已。并且如果update,delete有更新二级索引列的话,那么所有被更新的二级索引记录都会被加上X记录锁,这里实际上加的是隐式锁,效果与x记录锁相同。另外,对于最低两种隔离级别来讲,执行update语句采用的是半一致性读来完成的,后面我们会介绍半一致性读

上面我们说的都是锁定读的一般情况,下面介绍几种特殊情况(PS:真特么麻烦)

特殊情况:

  • 如果为最低的两种隔离级别,并且为精确匹配(有多个单点扫描区间),那么就不会为扫描记录的后一条记录加锁。
  • 如果为最高的两种隔离级别,并且为精确匹配,那么就会为扫描记录的后一条记录加上gap间隙锁。
  • 如果为最高的两种隔离级别,不是精确匹配,并且没有找到匹配的记录,则会为该扫描区间后面的一条记录加next key lock临键锁
  • 如果为最高的两种隔离级别,使用的是聚簇索引,并且是左闭区间,而且定位到的第一条记录恰好为扫描区间的记录,那么就为它加记录锁,而不是加临键锁
  • 无论哪个隔离级别,只要是唯一性匹配(简单来说就是只可能查询到一条记录),就会为读取到的记录加记录锁
  • 我们扫描某个区间的记录时,如果是按从右到左来扫描,那么会为第一条记录的下一条记录加gap锁

半一致性读

半一致性读是一种介于一致性读(快照读)和锁定读的读取方式

当隔离级别为最低两种时,且执行update语句时,则采用半一致性读

那什么叫半一致性读呢?

就是当update语句读取一条记录时,该记录已经被加了X锁,那么我们就会把它最新版本的记录读取出来,如果该版本记录的记录和搜索条件匹配,则再次读取记录并加锁;如果不匹配,则跳过该记录。这样处理让update尽量少被阻塞

insert

insert一般只是加了隐式锁,也就是不生成锁结构。不过如果插入的位置被加了gap锁,那么它就会生成插入意向锁,并进入等待状态。还有两种特殊情况会在内存中生成锁结构,这里我们就不讲了,有兴趣的同学自己翻书了解。

死锁

在InnoDB中有一个死锁检测机制,如果它检测到发生了死锁,那么它会选择一个较小的事务进行回滚(较小的事务就是事务执行过程中更新记录较少的事务),然后向客户端发送一个消息:Deadlock found when trying to get lock;try restarting transaction

一文掌握MySQL中的锁 创作打卡挑战赛 一文掌握MySQL中的锁 赢取流量/现金/CSDN周边激励大奖