(二)并发篇:锁是如何解决脏读,不可重复读,幻读的?

2024年 1月 5日 101.1k 0

(二)并发篇:锁是如何解决脏读,不可重复读,幻读的?

1. 全局锁介绍

2. 表级别锁介绍(表锁、MDL锁、意向锁)

3. 行级别锁介绍及案例统计(间隙锁、记录锁、临键锁)

4. 死锁分析和统计

前文回顾

在并发篇一中,我们提到了事务的特性**,以及事务的并发问题和隔离级别,以及快照读和当前读的区别;重点解释了快照读是如何避免安全问题的(通过MVCC);接下来,本章将围绕着当前读的安全问题应该如何避免!也就是加锁的机制

从粒度上划分锁,共有全局锁(锁数据库),表级别锁(锁表),行级别锁(只锁行数据)。其中,表级别锁有写锁,读锁,MDL锁,意向锁,插入自增锁等;行级别的锁有间隙锁,记录锁,临键锁、插入意向锁等。下面将从粗到细的粒度开始聊起。

image.png

全局锁

顾名思义,当执行全局锁时,整个数据库都会被锁住

执行命令: flush tables with read lock; 整个数据库都只处于只读状态,所有的增删改操作都会被阻塞,如下:在执行完锁库后,左边的客户端执行update发生阻塞。

image.png

表级别锁

表级别的锁分为普通的表锁,(读锁,写锁);MDL锁(元数据锁,分为读锁和写锁);意向锁(分为意向共享(IS)锁和意向独占(IX)锁)

如果是第一次接触的话,要先理解什么是读锁,什么是写锁?为什么好好的锁要这么分,还有独占锁和共享锁又是什么,乱七八糟的。

其实在并发当中,读和写是一个很重要的概念;我们对数据库的数据操作无非就两件事,一是读,二是改,如果整个数据库都不存在写操作的话,那根本不会有什么并发的问题(数据都至始至终都没有变,何来脏读,不可重复读,幻读?)

有了上面的概念,我们将锁区分为读锁和写锁。具体区分如下:

  • 因为读与读之间不会影响到数据的安全性。MySQL让读锁和读锁之间可以共享,即读读共享
  • 在读的时候如果有写操作乱入,则有可能会影响到数据的安全性。因此,MySQL让读锁和写锁互斥,有读无写,有写无读,读写互斥
  • 如果两个事务并发在写同一条数据,也有可能造成并发问题。因此,写锁和写锁之间也不能共存,即写写互斥

普通表锁

加锁形式为, 读(S):lock tables ... read; 写(X):lock tables ... write;

普通的表锁粒度大,完全不建议使用。一旦加了读锁,即使是本线程要执行写操作,也会提示失败,如下:

image.png

释放锁的命令为: unlock tables;

元数据锁(MDL)

元数据锁是一种隐式锁,不会显示的调用,对表做操作的时候会自动上锁,也分为MDL读锁和MDL写锁,满足读写互斥,读读共享,写写互斥。

MDL写锁:对表执行DDL语句时会加上(修改表结构)

MDL读锁:对表执行DML语句时会加上(普通的增删改查)

下面举一个MDL锁的例子,它也有可能会把自己弄出死锁来

image.png

一、在T2和T3时刻,两个不同的Session分别执行了一条普通的select语句,分别加上了MDL读锁,Session A的事务没有提交,读锁保留;而Session B已提交,读锁释放。

二、在T4时刻,Session C执行了一条DDL语句,给该表加上了MDL写锁,但由于前面已经有MDL读锁,又有读写互斥的特点。此刻Session C将发生阻塞

三、在T5时刻,新的Session D执行了一条MDL语句,给该表又加上了MDL读锁,但前面已经有Session C加的写锁,读写互斥。此刻Session D也会被阻塞

此刻死锁的事情即发生了!后续任何一个Session无论执行的是DML语句还是DDL语句,前面有写锁,这个表将完全不可读写!直到Session A将锁释放

意向锁

意向锁分为意向读锁和意向写锁,也称意向共享(IS)锁和意向独占(IX)锁。不是像其他锁那样,起到占有资源的作用,下面听我说来:

对于意向锁,我们重点关注它的加锁过程

一、 执行增删改操作的时候,会先对表加上意向独占锁,紧接着对表中的记录加独占锁(给记录上行锁)

二、 执行普通的select语句时,不会加意向锁。但执行select ... for update时,会对表加上意向独占锁,紧接着对行记录上独占锁;执行select ... lock in share mode时。

意向锁的特点:

  • 意向锁不会和行锁发生冲突,并且意向锁之间也可以共享,意向锁只会和普通的表级锁发生冲突。
  • 意向共享锁和普通表读锁共享,和普通表写锁互斥,满足IS、S共享,IS、X互斥。
  • 意向独占锁和普通表的读锁和写锁都互斥,满足IX、S互斥,IX、X互斥

意向锁存在的意义是啥?

从他的加锁过程来看,先上意向锁,再上实锁。,起到一个 “此路有人” 的作用

如果有意向锁的存在,在表锁即将对一张表上锁时,他就不用去遍历这张表是否有存在行锁,直接看他有没有意向锁就完事了,节约不必要的遍历开销。起到快速判断表里是否有锁记录的作用。

如果没有意向锁的话,在表锁要做上锁操作的时候,需要去遍历全表,存在不必要的开销。

插入自增锁(INC-AUTO)

插入自增锁的作用域:在表的主键是自增的条件下,执行insert操作

要理解插入自增锁,我们需要仔细剖析下insert操作:

  • 当执行一条普通的insert语句(不带主键),表需要为这条数据赋予主键(为当前表的主键+1)
  • 表需要先去执行申请主键的操作,在执行该操作,需要对其加上插入自增锁
  • 在加上插入自增锁后,表无法再为其他数据申请主键
  • 申请主键成功,释放锁。(不会等到语句执行完才释放,成功申请主键后立马释放)

如果是执行insert ... select这样的语句;插入自增锁会等到语句执行完才会释放,无法做到申请成功后立即释放

如果insert ... select也做到了申请主键后释放呢?情景如下:

image.png

一、在T1时刻,Session A往表t中插入了4条数据

二、在T2时刻,Session B参考表t,拷贝了一张表t2

三、在T3时刻,Session B执行insert ... select语句将表t的数据拷贝到表t2。与此同时,Session A往表t2又插入一条数据。

  • 表t2刚拷贝了两条数据(1,1,1)(2,2,2)
  • Session A往表t2插入了一条数据(3,5,5)
  • 表t2继续拷贝完剩下的数据(4,3,3)(5,4,4)

后续在做主从复制的时候,如果binlog_format = statement,由于binlog只会记录某张表的更新的sql,按照如下记录,并执行,肯定会造成主从数据不一致的问题

                    insert into t2 select c, d from t;
                    insert into t2 values (null, 5, 5);

因此,才要求在执行insert ... select时需要等执行完语句才释放插入自增锁

但其实MySQL也提供了可配置项,控制释放插入自增锁的时机,即:

image.png

当innodb_autoinc_lock_mode = 0时,所有的insert语句都只能在执行完语句后释放锁(性能最差)

当innodb_autoinc_lock_mode = 1时,普通的insert语句在申请完主键后释放锁,而insert ... select语句需要等语句执行完后才会释放锁(MySQL默认配置)

当innodb_autoinc_lock_mode = 2时,所有的insert语句在执行完语句后会释放锁(性能最好,但有数据不一致风险)

如果为了追求insert语句的性能,可以将主从备份的binlog_format设置为row,将完整的数据写到binlog中,而非只写sql(statement);再将innodb_autoinc_lock_mode设置为2。

行级别锁

行锁才是我们这次的重点~~ 行锁又可细分分为记录锁、间隙锁、临键锁(记录锁+间隙锁)、插入意向锁,在使用InnoDB引擎时,加锁的时机呢?这些锁又该怎么区分呢?加锁有没有什么规则?

什么时候会加行锁?

  • 情况一:执行增删改语句的时候,update insert delete;普通的select不会加锁

  • 情况二:执行select ... for update 查询语句会加独占锁;执行select ... lock in share mode查询语句会加共享锁

  • 记录锁、间隙锁、临键锁的特点

    行级别锁的加锁对象是索引,如果一张表里没有索引的话,就没有行锁的说法。直接升级为表锁(锁全表)

    记录锁: 一把记录锁只会锁住表里的一条行数据

    间隙锁: 锁住两条记录之间的间隙,(不会锁已存在表里的数据),防止幻读,往两条数据之间插入数据。

    临键锁:锁住一段间隙 + 行数据,(连续的)为 (.., ..],左开右闭的原则

    插入意向锁: 在执行insert语句时,会先判断当前位置是否有被上锁,有则生成插入意向锁,处于等待、阻塞状态

    加行锁的规则

    行锁只会锁住有访问的索引,并且上锁的基本单位为临键锁,但粒度有时会较大, 因此MySQL还存在以下优化原则:

    • 如果是主键、唯一键的等值查询,临键锁会退化为记录锁
    • 如果是索引的等值查询,从左到右遍历时,如果最后一个值不满足条件,会退化为间隙锁

    与此同时,在MySQL5.7版本中,还存在一个bug,(后续版本已经被修复)

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

    MySQL加行锁案例详解

    本文的重点来了! 会详细剖析在各种情况下,MySQL是如何对数据上行锁的,以下为后续案例的原始数据,场景如下:

    • MySQL的版本:5.7
    • MySQL的隔离级别:重复读(只有重复读才存在间隙锁和记录锁)
    • user表,表中有id(主键),name,age(普通索引),male四个字段

    初始数据如下:

    image.png

    案例如下:

    image.png

    一、非索引查询

    执行的sql语句如下:

                    select * from user where name = "test" for update;
    

    按照加锁规则,执行顺序如下:

  • name为非索引字段,查找的时候必然是全表扫描,为了保证不出现并发问题,会把所有的记录都锁住,等于行锁直接退化为表锁。
  • 关于该表的增删改操作,均会阻塞,直到上锁的事务提交。
  • 加锁的范围如下图:

    image.png

    二、主键索引的等值查询

    执行的sql语句如下:

                           select * from user where id = 5 for update;
    

    按照加锁规则,该sql的执行顺序如下:

  • id为主键,要筛选出id = 5的行数据,会访问主键索引树,找到id = 5的行数据
  • 加锁的单位为临键锁,因此会加上(1, 5]临键锁
  • 根据优化规则,该查询为主键等值查询,并且id = 5的记录存在,因此临键锁会退化为记录锁,最终加锁的范围为id = 5。
  • image.png

    假设存在多个事务并发操作,如下图:

    image.png

  • 在T2时刻,由上文分析,我们已知Session A会给id = 5的记录加上锁
  • 在T3时刻,Session B执行的关于id = 5的update语句,会发生阻塞
  • 在T4时刻,Session C执行的关于id = 3的update语句,不会阻塞。表明临键锁退化为记录锁
  • 紧接着,执行另一条主键等值查询的sql语句如下:

                           select * from user where id = 7 for update;
    

    id = 7这条记录不存在,该sql的执行顺序如下:

  • 由于id = 7的记录不存在。于是,落到了(5, 10)这个区间上。
  • 由于加锁的单位是临键锁,会向右遍历,找到右边第第一个不符合条件的值,即id = 10,加上(5, 10]的临键锁
  • 根据优化规则,索引等值查询的向右遍历,id = 10不符合条件,因此临键锁会退化为间隙锁,最终加锁的范围为 (5, 10)区间
  • image.png

    假设存在多个事务并发操作,如下图:

    image.png

  • 在T2时刻,由上文分析,我们已知Session A会给id ∈ (5, 10)的记录加上间隙锁
  • 在T3时刻,Session B执行的insert id = 7的语句,由于别的事务在这个区间上锁了,会发生阻塞
  • 在T4时刻,Session C执行的关于id = 10的update语句,不会阻塞。表明临键锁退化为间隙锁
  • 三、主键索引的范围查询

    执行两条sql语句如下,虽然返回的结果相同,但加的锁却完全不同!

                           select * from user where id = 10 for update;
                     select * from user where id >=10 and id < 11 for update;
    

    按照加锁规则,sql(1) 为值存在的主键索引等值查询,因此只会锁住id = 10这条记录

    对于sql(2)来说,属于主键索引的范围查询,加锁的执行顺序如下:

  • 由于id >= 10,会先进行等值查询,找到id = 10的行数据位置
  • 会给id = 10加上临键锁,即(5, 10],但又由于优化规则,会退化为id = 10的记录锁
  • 由于是范围查询,因此会向右遍历,找到id = 15这条记录,加上(10, 15]的临键锁。由于id = 15已经超过筛选范围,结束遍历。
  • 最终,该sql的上锁范围为:[10, 15]
  • image.png

    假设存在多个事务并发操作,如下图:

    image.png

  • 在T2时刻,由上文分析,我们已知Session A的加锁范围为:id ∈ [10, 15]
  • 在T3时刻,Session B执行的insert id = 8的语句,不会被阻塞,但 insert id = 12的语句,由于已经有Session A加的间隙锁,被阻塞住
  • 在T4时刻,Session C执行的update id = 15的语句,由于别的事务在id = 15的主键上加锁,会发生阻塞
  • 四、普通索引的等值查询

    执行两条sql语句如下,查询语句相同,但是加的锁不同,sql(1)是S锁,sql(2)是X锁

                        select id from user where age = 5 lock in share mode;
                           select id from user where age = 5 for update;
    

    对于sql(1)、(2)来说,属于普通索引的等值查询,加锁的执行顺序如下:

  • 索引的等值查询,会先走age的普通索引树,找到age = 5的位置,加上临键锁(1, 5]。
  • 由于是普通索引,值不具备唯一性,因此会向右遍历,判断是否还有age = 5的数据。
  • 找到age = 10的数据,不符合条件。但访问到了,还是会加上临键锁(5, 10]。由于是索引的等值查询遍历,会退化为间隙锁(5, 10)。
  • 只有访问到的对象才会加锁,sql(1)、(2)都用到了覆盖索引,不会回表去用主键索引。
    • 对于sql(1)来说,上的是S锁,不会去锁id = 5的数据(不会访问);
    • 对于sql(2)来说,上的是X锁,会顺便去锁数据对应的主键,即id = 5。
  • 最终,sql(1)的上锁范围为:age ∈ (1, 10);sql(2)的上锁范围为:age ∈ (1, 10),id = 5
  • image.png

    假设存在多个事务并发操作,如下图:

    image.png

  • 在T2时刻,由上文分析,我们已知Session A的加锁范围为:age ∈ (1, 10)
  • 在T3时刻,Session B执行的update id = 5的语句,不会被阻塞 (如果是X锁,就会阻塞)
  • 在T4时刻,Session C执行的insert id = 8的语句,由于别的事务在(5, 10)的区间上加了间隙锁,会发生阻塞
  • 五、普通索引的范围查询

    执行的sql语句如下:

                    select * from user where age >= 10 and age < 11 for update;
    

    对于上面的sql来说,属于普通索引的范围查询,加锁的执行顺序如下:

  • 由于是age >= 10,会先找到age = 10的记录所在位置。通过普通索引树找到age = 10的记录,并加上临键锁(5, 10]。不同于主键索引的等值查询,这里的普通索引的等值查询并不会让临键锁退化
  • 由于是范围查询,因此会向右遍历,找到age < 11的记录。于是找到了age = 15,加上临键锁(10, 15]
  • 由于age = 15已经不符合筛选条件,结束遍历。之后需要回表来补全除了id和age以外的行数据,因此id = 10的主键记录会被加上记录锁
  • 最终,sql的上锁范围为:age ∈ (5, 15], id = 10
  • image.png

    假设存在多个事务并发操作,如下图:

    image.png

  • 在T2时刻,由上文分析,我们已知Session A的加锁范围为:age ∈ (5, 15], id = 10
  • 在T3和T4时刻,Session B、C执行的update age = 8, 15的语句,由于Session A在(5, 15]的区间上加了锁,会发生阻塞
  • 在T4时刻,Session C执行的update id = 10的语句,由于Session A已经给id = 10上了记录锁,会发生阻塞
  • 六、普通索引(存在等值)的等值查询

    执行的sql语句如下:

                        select * from user where age = 10 for update;
    

    对于上面的sql来说,仍属于普通索引的等值查询,但表中有多条age = 10的记录,(临时补充一条id = 20, age = 10的数据),加锁的执行顺序如下:

  • 由于是等值查询,会先找到age = 10的记录所在位置。通过普通索引树找到(age = 10, id = 10) 的记录,并加上临键锁(5, 10]。
  • 由于是普通列(非唯一)的数据不具备唯一性,右边可能还会有相同的值,需要继续向右遍历查找。
  • 向右遍历,找到 (age = 20, id = 20),加上间隙锁
  • 继续向右遍历,找到age = 15,不符合条件,由于是索引的等值查询遍历,因此从age的(10, 15]临键锁退化为(10, 15)间隙锁。
  • 通过回表补齐数据,访问了两条age = 10的数据,因此主键id = 10, 20均会被加上记录锁。
  • 最终,sql的上锁范围为:age ∈ (5, 15), id = 10, 20
  • image.png

    假设存在多个事务并发操作,如下图:

    image.png

  • 在T2时刻,由上文分析,我们已知Session A的加锁范围为:age ∈ (5, 15), id = 10, 20
  • 在T3时刻,Session B执行的update age = 12的语句,由于Session A在(5, 15)的区间上加了锁,会发生阻塞
  • 在T4时刻,Session C执行的update age = 15的语句,由于Session A发生过锁退化,不会发生阻塞
  • 七、limit语句的加锁

    在上述的基础上,加了limit限制条件,执行的sql语句如下:

                      select * from user where age = 10 limit 2 for update;
    

    除开limit来看sql,仍属于普通索引的等值查询,加锁的执行顺序如下:

  • 由于是等值查询,会先找到age = 10的记录所在位置。通过普通索引树找到(age = 10, id = 10) 的记录,并加上临键锁(5, 10]。
  • 由于是普通列(非唯一)的数据不具备唯一性,右边可能还会有相同的值,需要继续向右遍历查找。
  • 向右遍历,找到 (age = 20, id = 20),加上间隙锁
  • 由于有limit 2的限制,最多只要两条数据即可,因此不会再继续查找下去。(如果是limit 3,就还会继续遍历下去)
  • 通过回表补齐数据,访问了两条age = 10的数据,因此主键id = 10, 20均会被加上记录锁。
  • 最终,sql的上锁范围为:age ∈ (5, 10], id = 10, 20
  • image.png

    假设存在多个事务并发操作,如下图:

    image.png

  • 在T2时刻,由上文分析,我们已知Session A的加锁范围为:age ∈ (5, 10], id = 10, 20
  • 在T3时刻,Session B执行的insert age = 12的语句,由于Session A没有访问到age = 15,因此在区间(10, 15)没有上锁,不会发生阻塞
  • 在T4时刻,Session C执行的insert age = 8的语句,由于Session A在(5, 10)上锁,会发生阻塞
  • 八、order by语句的加锁

    加了order by条件,执行的sql语句如下:

            select * from user where id > 9 and id < 12 order by id desc for update;
            select * from user where id > 9 and id < 12 order by id asc for update;
    

    同样的查询结果,排序顺序不同,则加的锁范围也会发生变化,上锁的顺序如下:

  • 对于sql(1),其为降序排序,因此需要从id < 12开始寻址,需要找到id = 12的左侧数据,但不存在,落到了间隙(10, 15)上,分析结果同案例二,会加上间隙锁(10, 15)。
  • 向左遍历,找到id = 10的数据,加上临键锁(5, 10],由于id = 10仍然满足 id > 9,因此会继续遍历,找到id = 5,加上加上临键锁(1, 5]。(此刻不满足条件,结束遍历)
  • image.png

  • 对于sql(2),其为升序排序,与sql(1)不同,此刻需要从id > 9开始寻址,需要找到id = 9的右侧数据,会落到间隙(5, 10)上
  • 向右遍历,找到id = 10的数据,加上临键锁(5, 10],继续向右遍历,找到id = 15,加上临键锁(10, 15]。(此刻不满足条件,结束遍历)
  • 最终,sql(1)的上锁范围为:id ∈ (1, 15],sql(2)的上锁范围为:id ∈ (5, 15]
  • image.png

    假设sql(1)存在多个事务并发操作,如下图:

    image.png

  • 在T2时刻,由上文分析,我们已知Session A的加锁范围为:id ∈ (1, 15]
  • 在T3时刻,Session B执行的update id = 8的语句,由于Session A访问到了id = 10,因此在区间(5, 10)上了锁,发生阻塞
  • 在T4时刻,Session C执行的insert id = 3的语句,由于Session A访问到了id = 5,因此在区间(1, 5)上了锁,发生阻塞
  • 假设sql(2)存在多个事务并发操作,如下图:

    image.png

  • 在T2时刻,由上文分析,我们已知Session A的加锁范围为:id ∈ (5, 15]
  • 不同的是,Session C执行的insert id = 3的语句,由于Session A没有访问到id = 5,因此没有在区间(1, 5)上锁,未发生阻塞
  • 九、MySQL5.7的加锁BUG案例

    执行的sql语句如下:

                select * from user where id > 10 and id <= 15 for update;
    

    唯一索引的范围查询会访问到不满足条件的第一个值为止,这句话该如何理解呢?

    先看看正常分析,上述的sql,上锁的顺序如下:

  • 该sql为主键索引的范围查询,从id > 10开始寻址,需要找到id = 10的左侧数据(不包含id = 10,不会访问到),落到了间隙(10, 15)上
  • 向右遍历,找到id = 15的数据,加上临键锁(10, 15],由于由于主键索引存在唯一性,id = 15的右边已经不会有满足筛选条件的值,但 15 ∈ (10, 15]。由于BUG的存在,会继续遍历,找到id = 20,加上加上临键锁(15, 20]。(此刻不满足条件,结束遍历)
  • 最终,sql的上锁范围为:id ∈ (10, 20]
  • image.png

    假设sql存在多个事务并发操作,如下图:

    image.png

  • 在T2时刻,由上文分析,由于存在BUG,Session A的加锁范围为:id ∈ (10, 20]
  • 在T3和T4时刻,Session B和C分别执行的update id = 20,insert id = 16的语句,由于Session A访问到了id = 20,因此在区间(15, 20]上了锁,都发生了阻塞
  • 十、锁范围突然变化

    在同一个事务内,原先加锁的范围可能是会受到其他事务干扰的,如下:

    存在两个Session并发执行,执行顺序如下:

    image.png

  • 在T2时刻,由上文分析的可知,Session A的加锁范围为:id ∈ (10, 20]
  • 在T3时刻,Session B执行了delete id = 10的语句,由于Session A上的锁没有到10,因此不会阻塞
  • 在删除id = 10后,id = 10的左右两个间隙,(5, 10)和(10, 15)将会合并成(5, 15)。因此,Session A的加锁范围升级为:id ∈ (5, 20]
  • 在T4时刻,Session B执行了insert id = 10的语句,由于Session A的锁范围发生了变化,因此会发生阻塞
  • 由update语句导致的变化,再看下面的例子:

    image.png

  • 在T2时刻,Session A执行的是普通索引的范围查询,由于是S锁,且又是覆盖索引查询,因此主键索引树不会加锁。最终加锁的范围是:age ∈ (5, +∞)
  • 在T3时刻,Session B执行的是update语句,update语句在底层被拆成(先插入新的,再删除旧的)。因此,先插入(id = 5,age = 1),再删除(id = 5,age = 5)。删除age = 5的数据后。Session A的上锁范围会扩大到(1, +∞)。
  • 在T4时刻,Session B再执行相反的update语句,等于先插入(id = 5,age = 5),再删除(id = 5,age = 1)。自然在插入的时候,age = 5会被区间(1, +∞)挡住。
  • 十一、由间隙锁引起的死锁案例

    首先我们要清楚一点,间隙锁的目的是什么? 间隙锁是为了防止别的事务在某个区间插入数据的,锁的是区间,不是具体的数据,主打一个预防的作用。因此,不同事务间的间隙锁是不互斥的。(ps: 记录锁互斥)

    由于间隙锁不互斥的特点,有可能会引发一些死锁的情况。存在两个Session并发执行,执行顺序如下:

    image.png

  • 在T2时刻,Session A执行的是普通索引的等值查询,由上文我们可知,会在age索引上(5, 10]的临键锁,(10, 15)的间隙锁
  • 在T3时刻,Session B执行了update age = 10的sql,执行update的语句其实分为几步:
    • 执行update age = 10,需要先做当前读,即同样需要在age索引上(5, 10]的临键锁,(10, 15)的间隙锁
    • 上临键锁的操作非原子性,需要分为两步:1. 先上(5, 10)间隙锁;2. 上age = 10的记录锁
    • 在上间隙锁的时候,由于间隙锁不具备互斥性,可顺利加上。而记录锁互斥,此刻会被阻塞住。间隙锁成功,记录锁失败
  • 在T4时刻,Session A执行了insert age = 8的语句,由于Session B已经在区间(5, 10)上了间隙锁,因此会发生阻塞
  • Session A被Session B的间隙锁(5, 10)困住;Session B被Session A的记录锁age = 10困住。死锁产生!

    十二、加锁导致的慢查询案例

    沿用原有的user表,数据一共就五条,在什么场景下,会导致慢查询?

    image.png

    场景如下,假设有两个事务并发执行

    image.png

  • 在T1时刻,Session A开启事务,并获取Read View(MVCC相关概念)
  • 在T2时刻,Session B对id = 1这条数据做了100W次修改,新增了100W个版本。
  • 在T3时刻,Session A执行普通的select id = 1的语句,为快照读,需要通过Read View去找到可见的id = 1的版本。而Session B执行的100W次操作都是不可见的,需要查找100W次才能找到符合版本的历史数据!
  • 在T4时刻,Session A直接执行当前读(直接看数据的最新版本记录),秒出结果!
  • 插入时加锁详解

    MySQL在执行insert语句时,是不会加显示锁的。只有在发生冲突了,才会加上显示锁(S或X)。当执行一条insert语句时,存在两种情况插入失败

    • 插入的位置有间隙锁,为了防止幻读出现,阻塞。
    • 出现主键冲突或者唯一键冲突

    image.png

    具体的插入失败案例如下:

    插入时被间隙锁挡住

    image.png

  • 在T2时刻,Session A执行的是普通索引的范围查询,user表中的age上锁区间为:(10, +∞)
  • 在T4时刻,Session B执行了insert age = 12的语句,会检查各个字段的对应区间是否有上锁,发现了age = 12对应的区间(10, 15)被上了间隙锁。因此,Session B则会在这里加上插入意向锁,处于等待状态
  • 存在主键冲突-1

    image.png

  • 在T2时刻,Session A执行insert id = 10的语句,由于数据库已有id = 10的数据,因此插入失败,主键冲突。此时发生了冲突,插入锁升级,需要给id = 10加上S型的临键锁,区间(5, 10]会被锁住。如果一个事务执行insert失败,则会给对应的记录加上S型锁,但不会阻塞本事务。
  • 在T4时刻,Session B执行insert id = 8的语句,由于Session A已在区间(5, 10]上了锁,因此会发生阻塞。
  • 在发生阻塞时,MySQL5.7版本可通过如下命令查看当前事务的锁信息

                        select * from information_schema.innodb_locksG;
    

    image.png

    可以看到,Session A持有(5, 10]的S型临键锁,而Session B被(5, 10)的间隙锁挡住

    存在主键冲突-2

    image.png

  • 在T2时刻,Session A执行insert id = 30的语句,数据库没有id = 30的数据,成功插入。上了隐式锁。此刻事务没有提交,隐式锁保留。
  • 在T4时刻,Session B同样执行insert id = 30的语句,由于Session A已经插入,尚未提交,则原来的隐式锁会直接升级成X型锁。而Session B想获得S型锁,必然造成锁冲突(X锁和S锁互斥),阻塞。
  • image.png

    可以看到,Session A持有(25, 30]的X型临键锁(发生了锁升级),而Session B需要申请id = 30的S型锁

    死锁的检验

    普通事务阻塞时:

    当事务发生阻塞时,在MySQL5.7版本,可用如下两条sql来具体分析:

                        select * from information_schema.innodb_locksG;
                      select * from information_schema.innodb_locks_waitsG;
    

    对于sql(1),执行结果如下:

    image.png

    对于sql(2),执行结果如下:

    image.png

    并且,当执行的sql发生了阻塞现象,通过如下命令查看连接中的线程

                                    show processlist;
    

    执行结果如下:

    image.png

    如上图情况,如果碰到线程阻塞的情况,可以直接通过暴力方法: kill query thread_id,来直接强制终止该sql。也可以直接通过:kill thread_id 直接杀死线程

    MySQL8.0中,不同于5.7版本,将原先的information_schema库中的innodb_lock_waits和innodb_locks表做了废除,用以下来替代:

                select * from performance_schema.data_lock_waits;
                select * from performance_schema.data_locks;
    

    在通过上述两条sql,也可查询到阻塞时加的锁信息,具体信息大同小异

    出现死锁情况时:

    如果我们平日里在使用数据库时出现了死锁,此刻MySQL对于死锁是有特殊处理的,会自动开启死锁检验。如果系统检测出了死锁,会自动回滚死锁中的某个事务

    由innodb_deadlock_detect决定,默认为ON,可自行修改

    image.png

    虽然MySQL会帮我们处理掉死锁的情况,但我们也需要了解一些分析死锁的基本问题,才能主动避免掉死锁。

    当出现两个线程互相等待锁时,即死锁情况。可通过如下命令查看锁情况

                                  show engine innodb statusG;
    

    该命令的作用是:查看innodb引擎当前的状态。如果发生死锁现象,可通过LATEST DETECTED DEADLOCK这一列查看具体的死锁信息

    针对上述的死锁案例,我们知道Session A和Session B互相被对方的锁阻挡,新开了一个Session C,来查看系统检测到的死锁情况,流程图如下:

    image.png

    LATEST DETECTED DEADLOCK的结果如下,Session B和C的事务情况分别如下:

    image.png

    image.png

    从结果可看出,详细记录了导致线程阻塞的sql,锁信息,锁类型,具体值

    最后是处理结果,如下:系统回滚了事务(1),即Session B,原因是此刻的Session A占有的锁资源更多,回滚成本更大。因此系统选择回滚成本小的事务

    image.png

    相关文章

    Oracle如何使用授予和撤销权限的语法和示例
    Awesome Project: 探索 MatrixOrigin 云原生分布式数据库
    下载丨66页PDF,云和恩墨技术通讯(2024年7月刊)
    社区版oceanbase安装
    Oracle 导出CSV工具-sqluldr2
    ETL数据集成丨快速将MySQL数据迁移至Doris数据库

    发布评论