zoukankan      html  css  js  c++  java
  • InnoDB中的锁

    背景

    最近看MySQL官方文档比较多,在此开坑翻译部分篇章,并附上一些旁注,用于展示实操结果,或者表达个人理解。

    文档版本:8.0
    来源:innodb-locking

    此类形式为旁注。
    本篇主要介绍InnoDB中的各类锁,而锁触发条件和应用场景不全在此篇中提及,后续会单独成篇进行讲解。

    共享锁 & 独占锁

    InnoDB 实现了两种类型的标准行锁:共享(S)锁和独占(X)锁。(下文简称S锁和X锁)

    • S锁允许持有该锁的事务读取一行记录
    • X锁允许持有该锁的事务更新或删除一行记录

    如果事务 T1 持有行 R 的S锁,另一个事务 T2 在行 R 上尝试获取锁,会有如下情景:

    • T2 请求 S 锁,可以直接获得。此时 T1 和 T2 都持有行 R 的S 锁。
    • T2 请求 X 锁,不能直接获得。

    如果T1 持有行 R 的 X锁,另一个事务 T2 在行 R 上尝试获取任何一种锁,都不能直接获得。T2 必须等待T1 释放行R 上的锁。

    这里讲到的S/X锁更倾向于在描述锁的模型:即锁的获取方式、资源控制能力和锁之间的交互。

    接下来所讲到的各类锁是基于S/X模型来实现的,不同在于粒度、强弱等。

    意向锁

    InnoDB支持多粒度锁:即行锁与表锁共存。例如语句LOCK TABLES ... WRITE 获取表的X锁。InnoDB使用 意向锁 实现在多个粒度
    上加锁。意向锁是表锁,用于指明一个事务稍后要获取哪种类型的行锁(S or X)。意向锁有两种类型:

    • 共享意向锁(IS):指明事务将要获取行的共享锁
    • 独占意向锁(IX):指明事务将要获取行的独占锁

    例如,SELECT ... FOR SHARE获取了 IS 锁,SELECT ... FOR UPDATE 获取了 IX 锁。

    注意5.65.7版本获取IS锁的语句有所不同:SELECT ... LOCK IN SHARE MODE

    意向锁的使用原则:
    一个事务若要获取行的 S 锁,必须先获取该表的 IS 锁或更强级别的锁。
    一个事务若要获取行的 X 锁,必须先获取该表的 IX 锁。

    表锁之间的兼容性总结如下:


    X IX S IS
    X 冲突 冲突 冲突 冲突
    IX 冲突 兼容 冲突 兼容
    S 冲突 冲突 兼容 兼容
    IS 冲突 兼容 兼容 兼容

    事务请求的锁必须和目前已产生的锁兼容,否则无法获取,直到冲突锁释放。而等待释放的过程如果InnoDB检测到存在死锁,则会抛出错误。

    意向锁不会阻塞其他锁请求,除了表锁(如 LOCK TABLES ... WRITE)。意向锁是为了表明事务正在尝试获取,或将要获取行锁。

    如果要查看当前数据库中的意向锁,执行 SHOW ENGINE INNODB STATUS , InnoDB 的监视器会输出如下内容:

    TABLE LOCK table `test`.`t` trx id 10080 lock mode IX
    

    如果在输出日志中看不到锁的相关信息,需要开启如下参数:
    SET GLOBAL innodb_status_output_locks=ON;
    见 : innodb-enabling-monitors

    记录锁

    记录锁用于锁住一条索引值。例如语句 SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE; 会防止其他事务针对t.c1=10的所有行进行增删改操作。

    记录锁只会锁住索引值,即使表中没有定义索引也是如此。如果没有索引,InnoDB会隐式创建一个聚簇索引,供记录锁锁定。

    如果要查看当前数据库中的记录锁,执行 SHOW ENGINE INNODB STATUS , InnoDB 的监视器会输出如下内容:

     RECORD LOCKS space id 58 page no 3 n bits 72 index `PRIMARY` of table `test`.`t`
     trx id 10078 lock_mode X locks rec but not gap
     Record lock, heap no 2 PHYSICAL RECORD: n_fields 3 ; compact format; info bits 0
     0 : len 4 ; hex 8000000 a; asc ;;
     1 : len 6 ; hex 00000000274 f; asc 'O;;
     2 : len 7 ; hex b60000019d0110; asc ;;
    

    trx id 10078 lock_mode X locks rec but not gap 意味记录锁只锁住了单条记录而没有锁定任何间隙,这也是通常主键查询的结果,关于间隙的概念下文中会介绍到。

    针对查询条件没有覆盖索引时的情况,进行实验:
    1.在一个表中加入自增主键,插入若干记录
    2.删除主键列的索引
    3.以原主键列的值作为查询条件执行SELECT FOR UPDATE

    监视器输出:RECORD LOCKS space id 3 page no 6 n bits 320 index GEN_CLUST_INDEX of table `test`.`t` trx id 2131 lock_mode X
    也就是使用了隐式生成的聚簇索引。
    在这种情况下即使查询条件中的列在值上是唯一的,也会锁定全表记录(因为走了全表扫描)。
    此时开启另一个事务,对另一条记录执行主键加锁查询(SELECT FOR UPDATE),根据S/X锁标准将被阻塞,经验证确实如此。

    间隙锁

    间隙锁用于锁定索引记录之间的间隙,或者一组索引值两端的间隙。比如语句 SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE;
    可以防止其他事务在t.c1列上插入 15 (因为在10-20之间),无论 15 是不是列上已有的值,因为在 BETWEEN 所指定的区间都被锁住了。

    所谓的间隙可以覆盖一个值,多个值,甚至是 0 个。

    关于间隙的准确含义此处引用官方术语集:
    间隙 指能在InnoDB索引数据结构中能被插入的位置。例如用SELECT ... FOR UPDATE 锁住一批行时,InnoDB将锁住条件命中的索引上的值以及它们之间的间隙。比如加锁读所有大于10的值时,间隙锁会防止其他事务插入大于10的值。

    间隙锁一定程度上体现了MySQL在性能与并发之间的权衡,在某些特定的事务隔离级别中使用到了间隙锁。

    对于使用唯一索引查找的语句,不会用到间隙锁(除非搜索条件中只包含一个多列唯一索引的部分列)例如下列语句中,如果列 id 有唯一索引,则只会用到一个 id=100 的记录锁,并且不会妨碍其它会话在之前的间隙进行插入。

    1 SELECT * FROM child WHERE id = 100 ;
    

    但如果id没有索引或者有一个非唯一索引,语句就会锁住之前的间隙。

    不同的事务可以在同一段间隙上持有相互冲突的锁。例如,事务A持有一段间隙的共享间隙锁(gap S-lock),同时事务B可以在同一段间隙上持有独占间隙锁(gap X-lock)。因为如果一条索引记录被删除,不同事务针对该记录持有的间隙锁必须被合并。

    在InnoDB中,间隙锁的互斥特性被相当程度地抑制了,意思是间隙锁的唯一作用是防止事务在间隙中进行插入操作。间隙锁可以共存。不同事务可以同时持有同一段间隙的间隙锁。共享间隙锁和独占间隙锁没有区别。它们之间不会冲突,且作用相同。

    间隙锁可以被显式禁用。通过改变事务隔离级别为 READ COMMITTED 或者开启系统变量 innodb_locks_unsafe_for_binlog(目前已弃用)
    来禁用。在这些情况下,间隙锁不再用于搜索和索引扫描,而只用于外键约束检查和重复键检查。

    上述的两种设置还有一些“副作用”。在MySQL计算出where条件后,不匹配的行的记录锁会被释放。对于UPDATE语句,InnoDB会执行一个“半一致性”读,从而向MySQL返回最新提交的版本号,用于挑选出匹配WHERE条件的行。

    针对最后一段提到关于 READ COMMITTED 会释放不匹配的记录锁进行实验。

    SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; 
    start TRANSACTION;  
    -- id未加索引  
    SELECT * from test.t where id =1 for update;
    

    监视器输出:

    RECORD LOCKS space id 3 page no 6 n bits 320 index >GEN_CLUST_INDEX of table `test`.`t` trx id 2178 lock_mode X locks rec but not gap
    

    可以看到虽然走聚簇索引,但事务最终只占有符合筛选的记录锁。
    隔离级别改回READ COMMITTED,执行相同语句。
    监视器输出:

    3 lock struct(s), heap size 1136, 372 row lock(s)  
    RECORD LOCKS space id 3 page no 6 n bits 320 index GEN_CLUST_INDEX of table `test`.`t` trx id 2179 lock_mode X
    

    占有全表记录锁。

    邻键锁

    邻键锁是记录锁以及在索引记录之前间隙上的间隙锁的组合。

    InnoDB在搜索或扫描索引时,对遇到的每一条索引记录设置共享/独占锁,以此实现行锁。因此,所谓的行锁实际就是记录锁。而邻键锁不仅仅锁住一条索引记录,还会影响记录之前的“间隙”。也就是说邻键锁可以表示为一个记录锁加上记录之前间隙的间隙锁。如果某个会话持有记录R上索引的共享/独占记录锁,对于其它会话,如果插入的值小于记录R上索引值(按索引排序规则),则不能直接插入,必须等待锁释放。

    设想一个索引包含值 10 , 11 , 13 , 20 。那么所有可能的邻键锁区间如下,圆括号代表不包含,方括号代表包含:

     (负无穷, 10 ]  
     ( 10 , 11 ]  
     ( 11 , 13 ]  
     ( 13 , 20 ]  
     ( 20 , 正无穷)  
    

    对于最后一个区间,邻键锁锁定一段大于最大索引值的间隙,并使用一个虚拟的纪录表示上界。这个上界并不是真实的索引值,所以实际上这个邻键锁没有携带记录锁,只有大于当前索引最大值的间隙锁。

    InnoDB默认使用 REPEATABLE READ 隔离级别。在这个级别下,InnoDB在搜索和扫描索引时使用邻键锁,用于避免幻行。

    如果要查看当前数据库中的邻键锁,执行 SHOW ENGINE INNODB STATUS , InnoDB 的监视器会输出如下内容:

     RECORD LOCKS space id 58 page no 3 n bits 72 index `PRIMARY` of table `test`.`t`
     trx id 10080 lock_mode X
     Record lock, heap no 1 PHYSICAL RECORD: n_fields 1 ; compact format; info bits 0
     0 : len 8 ; hex 73757072656 d756d; asc supremum;;
     Record lock, heap no 2 PHYSICAL RECORD: n_fields 3 ; compact format; info bits 0
     0 : len 4 ; hex 8000000 a; asc ;;
     1 : len 6 ; hex 00000000274 f; asc 'O;;
     2 : len 7 ; hex b60000019d0110; asc ;;
    

    邻键锁主要用于解决幻行问题:事务因其它事务的插入操作导致两次读取的结果集不一致。邻键锁解决了这一问题,可帮助应用实现插入值唯一(加锁读->获取邻键锁->只允许本会话插入)。

    插入意向锁

    插入意向锁是一种特殊的间隙锁,在插入操作中执行行插入之前获得。用于标志插入的意向,从而使多个事务在同一段间隙执行插入时,如果对方不在同一个索引值位置上插入,则无需互相等待。例如,当前索引值有 4 和 7 。两个事务分别准备插入 5 和 6 ,在获取被插入行的独占锁之前,它们会各自获取 4 至 7 之间间隙的插入意向锁,且不会互相阻塞,因为插入值没有冲突。

    下面通过一个例子来演示事务在获取记录的独占锁之前,获取插入意向锁的过程。案例中涉及两个客户端,分别是A和B。

    客户端A创建一张表,包含两条索引值( 90 和 102),然后开启一个事务,获取ID大于100的所有记录的独占锁。独占锁将包含一个 id<102 的间隙锁:

    即邻键锁,区间 =(100,102]

     mysql> CREATE TABLE child (id int( 11 ) NOT NULL, PRIMARY KEY(id)) ENGINE=InnoDB;
    
     mysql> INSERT INTO child (id) values ( 90 ),( 102 );
     
     mysql> START TRANSACTION;
     
     mysql> SELECT * FROM child WHERE id > 100 FOR UPDATE;
     
     +-----+
     | id  |
     +-----+
     | 102 |
     +-----+
    

    客户端B开启一个事务,在间隙内执行插入记录的命令。事务会先获取一个插入意向锁,然后等待获取独占锁。

     mysql> START TRANSACTION;
     mysql> INSERT INTO child (id) VALUES ( 101 );
    

    如果要查看当前数据库中的插入意向锁,执行 SHOW ENGINE INNODB STATUS , InnoDB 的监视器会输出如下内容:

    1 RECORD LOCKS space id 31 page no 3 n bits 72 index `PRIMARY` of table `test`.`child`
    2 trx id 8731 lock_mode X locks gap before rec insert intention waiting
    3 Record lock, heap no 3 PHYSICAL RECORD: n_fields 3 ; compact format; info bits 0
    4 0 : len 4 ; hex 80000066 ; asc f;;
    5 1 : len 6 ; hex 000000002215 ; asc " ;;
    6 2 : len 7 ; hex 9000000172011 c; asc r ;;...
    

    自增锁

    自增锁是一种特殊的表锁,事务在带有自增列的表中执行插入会获取自增锁。在最简单的情形中,如果某个事务在向表中插入数据,其它事务必须等待其插入完毕才能执行自己的插入,以此来保证主键值是连续的。

    配置项 innodb_autoinc_lock_mode 用于控制自增锁使用的算法,以帮助你在自增序列的可预测性和插入的并发能力之间权衡。

    针对空间索引的断言锁

    InnoDB支持对空间行建立索引。
    在处理涉及到空间索引的操作时,邻键锁在 REPEATABLE READSERIALIZABLE 两个隔离级别上不能很好的工作。因为对于多维数据没有绝对的排序规则,所以并不能明确谁才是“邻键”。

  • 相关阅读:
    Android:JNI与NDK(一)
    okio:定义简短高效
    hashCode()方法以及集合中Set的一些总结
    Gradle入门到实战(二) — ImageOptimization安卓图片转换压缩插件
    Gradle入门到实战(一) — 全面了解Gradle
    数据结构与算法(十二):八大经典排序算法再回顾
    数据结构与算法(十一):图的基础以及遍历代码实现
    数据结构与算法(十):红黑树与TreeMap详细解析
    数据结构与算法(九):AVL树详细讲解
    Android版数据结构与算法(八):二叉排序树
  • 原文地址:https://www.cnblogs.com/notayeser/p/13943097.html
Copyright © 2011-2022 走看看