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

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

  1. 全局锁介绍

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

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

  4. 死锁分析和统计

前文回顾

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

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

全局锁

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

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

表级别锁

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

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

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

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

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

普通表锁

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

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

释放锁的命令为: unlock tables;

元数据锁(MDL)

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

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

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

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

一、在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也做到了申请主键后释放呢?情景如下:

一、在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,按照如下记录,并执行,肯定会造成主从数据不一致的问题

sql 复制代码
                    insert into t2 select c, d from t;
                    insert into t2 values (null, 5, 5);

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

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

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引擎时,加锁的时机呢 ?这些锁又该怎么区分 呢?加锁有没有什么规则

什么时候会加行锁?

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

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

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

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

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

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

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

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

加行锁的规则

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

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

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

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

MySQL加行锁案例详解

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

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

初始数据如下:

案例如下:

一、非索引查询

执行的sql语句如下:

sql 复制代码
                select * from user where name = "test" for update;

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

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

加锁的范围如下图:

二、主键索引的等值查询

执行的sql语句如下:

sql 复制代码
                       select * from user where id = 5 for update;

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

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

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

  1. 在T2时刻,由上文分析,我们已知Session A会给id = 5的记录加上锁
  2. 在T3时刻,Session B执行的关于id = 5的update语句,会发生阻塞
  3. 在T4时刻,Session C执行的关于id = 3的update语句,不会阻塞表明临键锁退化为记录锁

紧接着,执行另一条主键等值查询的sql语句如下:

sql 复制代码
                       select * from user where id = 7 for update;

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

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

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

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

三、主键索引的范围查询

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

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)来说,属于主键索引的范围查询加锁的执行顺序如下:

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

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

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

四、普通索引的等值查询

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

sql 复制代码
                    select id from user where age = 5 lock in share mode;
                       select id from user where age = 5 for update;

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

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

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

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

五、普通索引的范围查询

执行的sql语句如下:

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

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

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

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

  1. 在T2时刻,由上文分析,我们已知Session A的加锁范围为:age ∈ (5, 15], id = 10
  2. 在T3和T4时刻,Session B、C执行的update age = 8, 15的语句,由于Session A在(5, 15]的区间上加了锁,会发生阻塞
  3. 在T4时刻,Session C执行的update id = 10的语句,由于Session A已经给id = 10上了记录锁,会发生阻塞

六、普通索引(存在等值)的等值查询

执行的sql语句如下:

sql 复制代码
                    select * from user where age = 10 for update;

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

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

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

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

七、limit语句的加锁

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

sql 复制代码
                  select * from user where age = 10 limit 2 for update;

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

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

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

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

八、order by语句的加锁

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

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;

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

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

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

  1. 在T2时刻,由上文分析,我们已知Session A的加锁范围为:id ∈ (1, 15]
  2. 在T3时刻,Session B执行的update id = 8的语句,由于Session A访问到了id = 10,因此在区间(5, 10)上了锁,发生阻塞
  3. 在T4时刻,Session C执行的insert id = 3的语句,由于Session A访问到了id = 5,因此在区间(1, 5)上了锁,发生阻塞

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

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

九、MySQL5.7的加锁BUG案例

执行的sql语句如下:

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

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

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

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

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

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

十、锁范围突然变化

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

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

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

update语句导致的变化,再看下面的例子:

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

十一、由间隙锁引起的死锁案例

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

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

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

Session A被Session B的间隙锁(5, 10)困住;Session B被Session A的记录锁age = 10困住。死锁产生!

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

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

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

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

插入时加锁详解

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

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

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

插入时被间隙锁挡住

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

存在主键冲突-1

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

在发生阻塞时,MySQL5.7版本 可通过如下命令查看当前事务的锁信息

csharp 复制代码
                    select * from information_schema.innodb_locks\G;

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

存在主键冲突-2

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

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

死锁的检验

普通事务阻塞时:

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

csharp 复制代码
                    select * from information_schema.innodb_locks\G;
                  select * from information_schema.innodb_locks_waits\G;

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

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

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

ini 复制代码
                                show processlist;

执行结果如下:

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

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

csharp 复制代码
            select * from performance_schema.data_lock_waits;
            select * from performance_schema.data_locks;

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

出现死锁情况时:

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

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

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

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

ini 复制代码
                              show engine innodb status\G;

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

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

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

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

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

相关推荐
CoderIsArt1 小时前
Redis的三种模式:主从模式,哨兵与集群模式
数据库·redis·缓存
师太,答应老衲吧3 小时前
SQL实战训练之,力扣:2020. 无流量的帐户数(递归)
数据库·sql·leetcode
Yaml44 小时前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍
Channing Lewis4 小时前
salesforce case可以新建一个roll up 字段,统计出这个case下的email数量吗
数据库·salesforce
追风林4 小时前
mac 本地docker-mysql主从复制部署
mysql·macos·docker
毕业设计制作和分享5 小时前
ssm《数据库系统原理》课程平台的设计与实现+vue
前端·数据库·vue.js·oracle·mybatis
ketil275 小时前
Redis - String 字符串
数据库·redis·缓存
Hsu_kk6 小时前
MySQL 批量删除海量数据的几种方法
数据库·mysql
编程学无止境6 小时前
第02章 MySQL环境搭建
数据库·mysql
knight-n6 小时前
MYSQL库的操作
数据库·mysql