美文网首页
(MySQL死锁认识二)数据库中的锁

(MySQL死锁认识二)数据库中的锁

作者: 多喝岩浆y | 来源:发表于2021-09-23 14:53 被阅读0次

    数据库中的锁按照不同的方式区分有不同的结果,按照锁的粒度区分,可分为表级锁和行级锁

    表锁和行锁

    表锁意为对整张表加锁,其他的事务无法对表再进行修改,行锁即只锁定某些行上的内容,多个事务可以同时操作互补干扰的行。

    表锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。

    行锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。

    对于行锁来说,MySQL是通过对索引加锁实现的,如果某个表中没有索引,数据库会默认包含一个索引来进行处理,所以为了了解行锁,我们必须要对索引有基本的认识

    MySQL的索引

    注:这里我们主要分析innodb存储引擎的索引实现(MyISAM的实现方式不同)

    InnoDB存储引擎存放数据的底层数据结构使用的是B+树,B+树有三个优点,所以使用InnoDB存储引擎也附带着有它的特点

    B+ 树的三个优点

    1)层级更低,IO 次数更少每次都需要查询到叶子节点

    2)查询性能稳定叶子节点形成有序链表

    3)范围查询方便

    InnoDb 存储引擎的数据是保存在主键索引里的,主键索引中会存这行内容的所有数据,非主键索引里保存着该节点对应的主键,如果想要找到这行数据,那么需要再根据主键去主键索引中再查找一次。

    主键索引

    基于主键索引和非主键索引的这种特点,当我们是基于主键去锁记录的时候,锁住的就是具体的主键索引中的那些行;如果基于非主键索引锁记录,那么会锁住非主键索引,然后还会锁住这些非主键索引对应的主键索引。

    假设我们有个学生表,包含了id、name、score字段,其中设置id为主键,并未name设置了索引,当我们执行如下SQL时

    mysql> update students set score = 100 where id = 49;

    那么InnoDb 存储引擎会在 id = 49 这个主键索引上加一把写锁。

    当我们执行如下SQL时

    mysql> update students set score = 100 where name = 'Tom';

    那么首先会给name为Tom的非主键索引加锁,然后为这个非主键索引对应的主键索引加锁

    非主键索引对主键索引加锁

    这里还要提一个MySQL和Mariadb(MySQL社区版)不同的地方,当使用Mariadb进行批量更新的时候,数据库底层会使用缓存进行优化。

    例如当执行如下SQL时:UPDATE employees SET salary = salary+100 WHERE salary < 2000;

    数据库会先算出满足这些条件的行,然后将其缓存下来,全部获取完成之后,再进行更新(官网文档地址:https://mariadb.com/kb/en/using-buffer-update-algorithm/)

    行锁的分类

    在MySQL中,对于行锁也定义了四种类型

    LOCK_ORDINARY:也称为 Next-Key Lock,锁一条记录及其之前的间隙,这是 RR 隔离级别用的最多的锁,从名字也能看出来。

    LOCK_GAP:间隙锁,锁两个记录之间的 GAP,防止记录插入。

    LOCK_REC_NOT_GAP:只锁记录。

    LOCK_INSERT_INTENSION:插入意向 GAP 锁,插入记录时使用,是 LOCK_GAP 的一种特例。

    1)LOCK_ORDINARY

    记录锁 是最简单的行锁,并没有什么好说的。譬如下面的 SQL 语句(id 为主键):

    mysql> UPDATE accounts SET level = 100 WHERE id = 5;

    这条 SQL 语句就会在 id = 5 这条记录上加上记录锁,防止其他事务对 id = 5 这条记录进行修改或删除。记录锁永远都是加在索引上的,就算一个表没有建索引,数据库也会隐式的创建一个索引。如果 WHERE 条件中指定的列是个二级索引,那么记录锁不仅会加在这个二级索引上,还会加在这个二级索引所对应的聚簇索引上。

    注意,如果 SQL 语句无法使用索引时会走主索引实现全表扫描,这个时候 MySQL 会给整张表的所有数据行加记录锁。如果一个 WHERE 条件无法通过索引快速过滤,存储引擎层面就会将所有记录加锁后返回,再由 MySQL Server 层进行过滤。不过在实际使用过程中,MySQL 做了一些改进,在 MySQL Server 层进行过滤的时候,如果发现不满足,会调用 unlock_row 方法,把不满足条件的记录释放锁(显然这违背了二段锁协议)。这样做,保证了最后只会持有满足条件记录上的锁,但是每条记录的加锁操作还是不能省略的。可见在没有索引时,不仅会消耗大量的锁资源,增加数据库的开销,而且极大的降低了数据库的并发性能,所以说,更新操作一定要记得走索引。

    2)LOCK_GAP

    还是看上面的那个例子,如果 id = 5 这条记录不存在,这个 SQL 语句还会加锁吗?答案是可能有,这取决于数据库的隔离级别。

    对于事务可能会出现的问题中,有一个问题叫做 幻读,指的是在同一个事务中同一条 SQL 语句连续两次读取出来的结果集不一样。在 read committed 隔离级别很明显存在幻读问题,在 repeatable read 级别下,标准的 SQL 规范中也是存在幻读问题的,但是在 MySQL 的实现中,使用了间隙锁的技术避免了幻读。

    间隙锁 是一种加在两个索引之间的锁,或者加在第一个索引之前,或最后一个索引之后的间隙。有时候又称为范围锁(Range Locks),这个范围可以跨一个索引记录,多个索引记录,甚至是空的。使用间隙锁可以防止其他事务在这个范围内插入或修改记录,保证两次读取这个范围内的记录不会变,从而不会出现幻读现象。很显然,间隙锁会增加数据库的开销,虽然解决了幻读问题,但是数据库的并发性一样受到了影响,所以在选择数据库的隔离级别时,要注意权衡性能和并发性,根据实际情况考虑是否需要使用间隙锁,大多数情况下使用 read committed 隔离级别就足够了,对很多应用程序来说,幻读也不是什么大问题。

    回到这个例子,这个 SQL 语句在 RC 隔离级别不会加任何锁,在 RR 隔离级别会在 id = 5 前后两个索引之间加上间隙锁。

    值得注意的是,间隙锁和间隙锁之间是互不冲突的,间隙锁唯一的作用就是为了防止其他事务的插入,所以加间隙写锁和加间隙读锁没有任何区别。

    3)LOCK_REC_NOT_GAP

    Next-key 锁 是记录锁和间隙锁的组合,它指的是加在某条记录以及这条记录前面间隙上的锁。假设一个索引包含10、11、13 和 20 这几个值,可能的 Next-key 锁如下:

    (-∞, 10]、(10, 11]、(11, 13]、(13, 20]、(20, +∞)

    通常我们都用这种左开右闭区间来表示 Next-key 锁,其中,圆括号表示不包含该记录,方括号表示包含该记录。前面四个都是 Next-key 锁,最后一个为间隙锁。和间隙锁一样,在 RC 隔离级别下没有 Next-key 锁,只有 RR 隔离级别才有。继续拿上面的 SQL 例子来说,如果 id 不是主键,而是二级索引,且不是唯一索引,那么这个 SQL 在 RR 隔离级别下会加什么锁呢?答案就是 Next-key 锁,如下:

    (a, 5]、(5, b)

    其中,a 和 b 是 id = 5 前后两个索引,我们假设 a = 1、b = 10,那么此时如果插入一条 id = 3 的记录将会阻塞住。之所以要把 id = 5 前后的间隙都锁住,仍然是为了解决幻读问题,因为 id 是非唯一索引,所以 id = 5 可能会有多条记录,为了防止再插入一条 id = 5 的记录,必须将下面标记 ^ 的位置都锁住,因为这些位置都可能再插入一条 id = 5 的记录:

    1 ^ 5 ^ 5 ^ 5 ^ 10 11 13 15

    可以看出来,Next-key 锁确实可以避免幻读,但是带来的副作用是连插入 id = 3 这样的记录也被阻塞了,这根本就不会引起幻读问题的。

    4)LOCK_INSERT_INTENSION

    插入意向锁 是一种特殊的间隙锁(所以有的地方把它简写成 II GAP),这个锁表示插入的意向,只有在 INSERT 的时候才会有这个锁。插入意向锁和插入意向锁之间互不冲突,所以可以在同一个间隙中有多个事务同时插入不同索引的记录。譬如在上面的例子中,id = 1 和 id = 5 之间如果有两个事务要同时分别插入 id = 2 和 id = 3 是没问题的,虽然两个事务都会在 id = 1 和 id = 5 之间加上插入意向锁,但是不会冲突。

    插入意向锁只会和间隙锁或 Next-key 锁冲突,正如上面所说,间隙锁唯一的作用就是防止其他事务插入记录造成幻读,那么间隙锁是如何防止幻读的呢?正是由于在执行 INSERT 语句时需要加插入意向锁,而插入意向锁和间隙锁冲突,从而阻止了插入操作的执行。

    整理一下不同锁之间的冲突,如下表所示(第一行表示已经加上的锁、第一列表示要加的锁)

    锁冲突

    对于提到的这个表格,我们可以捋一下便于理解

    1)记录锁只和记录锁以及Next-Key锁冲突

    2)间隙锁只和插入意向锁冲突

    3)Next-key锁只和间隙锁兼容

    4)插入意向锁和其他锁都兼容

    相关文章

      网友评论

          本文标题:(MySQL死锁认识二)数据库中的锁

          本文链接:https://www.haomeiwen.com/subject/ibangltx.html