MySQL · 引擎特性 · InnoDB 事务锁系统简介(上)

一 序

    最近跟速运联调系统,没顾上写笔记。之前看沈剑老师的公众号:架构师之路 有对锁做了介绍。还是参照官方文档整理下,有助于系统理解。主要是mysql.taobao的月报。

    锁机制是数据库区别于文件系统的主要标志之一,用于管理对共享资源的并发访问。MYSQL支持表锁,innodb引擎支持行锁,主要分为基本概念,目的是了解锁类型,加锁场景。case没有补充。代码以mysql 5.7.17版本。

二 锁

先看官网介绍:

This section describes lock types used by InnoDB.

比常见的说法七种锁还多一种,最后一种看官网介绍。

共享排他锁(Shared and Exclusive Locks )

InnoDB implements standard row-level locking where there are two types of locks, shared (S) locks and exclusive (X) locks.

  • shared (S) lock permits the transaction that holds the lock to read a row.

  • An exclusive (X) lock permits the transaction that holds the lock to update or delete a row.

如果一个事务对某一行数据加了S锁,另一个事务还可以对相应的行加S锁,但是不能对相应的行加X锁。
如果一个事务对某一行数据加了X锁,另一个事务既不能对相应的行加S锁也不能加X锁。

记录锁(Record Locks):

      A record lock is a lock on an index record. For example, SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE; prevents any other transaction from inserting, updating, or deleting rows where the value of t.c1 is 10.

      Record locks always lock index records, even if a table is defined with no indexes. For such cases, InnoDB creates a hidden clustered index and uses this index for record locking. See Section 14.8.2.1, “Clustered and Secondary Indexes”.

记录锁锁定索引中一条记录。在 RC 隔离级别下一般加的都是该类型的记录锁(但唯一二级索引上的 duplicate key 检查除外,总是加 LOCK_ORDINARY 类型的锁)。

间隙锁(Gap Locks)

       A gap lock is a lock on a gap between index records, or a lock on the gap before the first or after the last index record. For example, SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE; prevents other transactions from inserting a value of 15 into column t.c1, whether or not there was already any such value in the column, because the gaps between all existing values in the range are locked.

    表示只锁住一段范围,不锁记录本身,通常表示两个索引记录之间,或者索引上的第一条记录之前,或者最后一条记录之后的锁。可以理解为一种区间锁,一般在RR隔离级别下会使用到GAP锁。

Next-Key Locks

      A next-key lock is a combination of a record lock on the index record and a gap lock on the gap before the index record.

  InnoDB performs row-level locking in such a way that when it searches or scans a table index, it sets shared or exclusive locks on the index records it encounters. Thus, the row-level locks are actually index-record locks. A next-key lock on an index record also affects the “gap” before that index record. That is, a next-key lock is an index-record lock plus a gap lock on the gap preceding the index record. If one session has a shared or exclusive lock on record R in an index, another session cannot insert a new index record in the gap immediately before R in the index order.

  Next-Key锁是索引记录上的记录锁和在索引记录之前的间隙锁的组合。

     Suppose that an index contains the values 10, 11, 13, and 20. The possible next-key locks for this index cover the following intervals, where a round bracket denotes exclusion of the interval endpoint and a square bracket denotes inclusion of the endpoint:negative infinity, 10] (10, 11] (11, 13] (13, 20] (20, positive infinity)

     上面是例子假设有数据为10,11,13,20,用集合的方式表示为(-∞ ,10],(10,11],(11,13],(20,∞)

     当前 MySQL 默认情况下使用RR的隔离级别,而NEXT-KEY LOCK正是为了解决RR隔离级别下的幻读问题。所谓幻读就是一个事务内执行相同的查询,会看到不同的行记录。在RR隔离级别下这是不允许的。假设索引上有记录1, 4, 5, 8,12 我们执行类似语句:SELECT… WHERE col > 10 FOR UPDATE。如果我们不在(8, 12)之间加上Gap锁,另外一个 Session 就可能向其中插入一条记录,例如9,再执行一次相同的SELECT FOR UPDATE,就会看到新插入的记录。

意向锁(Intention Locks)

  InnoDB supports multiple granularity locking which permits coexistence of row locks and table locks. For example, a statement such asLOCK TABLES ... WRITE takes an exclusive lock (an X lock) on the specified table. To make locking at multiple granularity levels practical, InnoDB uses intention locks. Intention locks are table-level locks that indicate which type of lock (shared or exclusive) a transaction requires later for a row in a table. There are two types of intention locks:

  • An intention shared lock (IS) indicates that a transaction intends to set a shared lock on individual rows in a table.

  • An intention exclusive lock (IX) indicates that that a transaction intends to set an exclusive lock on individual rows in a table.

For example, SELECT ... LOCK IN SHARE MODE sets an IS lock, and SELECT ... FOR UPDATE sets an IX lock.

The intention locking protocol is as follows:

  • Before a transaction can acquire a shared lock on a row in a table, it must first acquire an IS lock or stronger on the table.

  • Before a transaction can acquire an exclusive lock on a row in a table, it must first acquire an IX lock on the table.

Table-level lock type compatibility is summarized in the following matrix.意向锁兼容性

  X IX S IS
X Conflict Conflict Conflict Conflict
IX Conflict Compatible Conflict Compatible
S Conflict Conflict Compatible Compatible
IS Conflict Compatible Compatible Compatible

      意向锁是一种表级锁,锁的粒度是整张表,分为意向共享锁(IS)和意向排它锁(IX)。

      意向锁之间彼此不会冲突,因为它们都只是“有意”,而不是真干,所以是可以兼容的。在加行锁之前,会使用意向锁判断是否冲突;关于上面的兼容性:IX和X的关系等同于X和X之间的关系,为什么呢?因为事务获得了IX锁,接下来就有权利获取X锁,这样就会出现两个事务都获取X锁的情况,这和我们已知的X锁和X锁之间互斥是矛盾的;

  引入意向锁的目的:在于在定位到特定的行所持有的锁之前,提供一种更粗粒度的锁,可以大大节约引擎对于锁的定位和处理的性能,因为在存储引擎内部,锁是由一块独立的数据结构维护的,锁的数量直接决定了内存的消耗和并发性能。例如,事务A对表t的某些行修改(DML通常会产生X锁),需要对t加上意向排它锁,在A事务完成之前,B事务来一个全表操作(alter table等),此时直接在表级别的意向排它锁就能告诉B需要等待(因为t上有意向锁),而不需要再去行级别判断。 

插入意向锁( Insert Intention Locks)

        An insert intention lock is a type of gap lock set by INSERT operations prior to row insertion. This lock signals the intent to insert in such a way that multiple transactions inserting into the same index gap need not wait for each other if they are not inserting at the same position within the gap. Suppose that there are index records with values of 4 and 7. Separate transactions that attempt to insert values of 5 and 6, respectively, each lock the gap between 4 and 7 with insert intention locks prior to obtaining the exclusive lock on the inserted row, but do not block each other because the rows are nonconflicting.

      INSERT INTENTION锁是GAP锁的一种,如果有多个session插入同一个GAP时,他们无需互相等待,例如当前索引上有记录4和8,两个并发session同时插入记录6,7。他们会分别为(4,8)加上GAP锁,但相互之间并不冲突(因为插入的记录不冲突)。
       当向某个数据页中插入一条记录时,总是会调用函数lock_rec_insert_check_and_lock进行锁检查(构建索引时的数据插入除外),会去检查当前插入位置的下一条记录上是否存在锁对象,这里的下一条记录不是指的物理连续,而是按照逻辑顺序的下一条记录。 如果下一条记录上不存在锁对象:若记录是二级索引上的,先更新二级索引页上的最大事务ID为当前事务的ID;直接返回成功。
      如果下一条记录上存在锁对象,就需要判断该锁对象是否锁住了GAP。如果GAP被锁住了,并判定和插入意向GAP锁冲突,当前操作就需要等待,加的锁类型为LOCK_X | LOCK_GAP | LOCK_INSERT_INTENTION,并进入等待状态。但是插入意向锁之间并不互斥。这意味着在同一个GAP里可能有多个申请插入意向锁的会话。

自增锁(AUTO-INC Locks)

      An AUTO-INC lock is a special table-level lock taken by transactions inserting into tables with AUTO_INCREMENT columns. In the simplest case, if one transaction is inserting values into the table, any other transactions must wait to do their own inserts into that table, so that rows inserted by the first transaction receive consecutive primary key values.

       The innodb_autoinc_lock_mode configuration option controls the algorithm used for auto-increment locking. It allows you to choose how to trade off between predictable sequences of auto-increment values and maximum concurrency for insert operations.

      AUTO_INC锁加在表级别,和AUTO_INC、表级S锁以及X锁不相容。锁的范围为SQL级别,SQL结束后即释放。AUTO_INC的加锁逻辑和InnoDB的锁模式相关,这里在简单介绍一下。

    通常对于自增列,我们既可以显式指定该值,也可以直接用NULL,系统将自动递增并填充该列。我们还可以在批量插入时混合使用者两种方式。不同的分配方式,其具体行为受到参数innodb_autoinc_lock_mode的影响。但在基于STATEMENT模式复制时,可能会影响到复制的数据一致性,官方文档 有详细描述.

Predicate Locks for Spatial Indexes

InnoDB supports SPATIAL indexing of columns containing spatial columns (see Section 11.5.8, “Optimizing Spatial Analysis”).

To handle locking for operations involving SPATIAL indexes, next-key locking does not work well to support REPEATABLE READ orSERIALIZABLE transaction isolation levels. There is no absolute ordering concept in multidimensional data, so it is not clear which is the “next” key.

To enable support of isolation levels for tables with SPATIAL indexes, InnoDB uses predicate locks. A SPATIAL index contains minimum bounding rectangle (MBR) values, so InnoDB enforces consistent read on the index by setting a predicate lock on the MBR value used for a query. Other transactions cannot insert or modify a row that would match the query condition。

     从 MySQL5.7 开始MySQL整合了boost.geometry库以更好的支持空间数据类型,并支持在在Spatial数据类型的列上构建索引,在InnoDB内,这个索引和普通的索引有所不同,基于R-TREE的结构,目前支持对2D数据的描述,暂不支持3D。非行业相关用不到吧。关于Predicate Lock的设计参阅官方WL#6609

三 加锁场景

 3.1 共享锁 (lock_S)

    共享锁的作用通常用于在事务中读取一条行记录后,不希望它被别的事务锁修改,但所有的读请求产生的LOCK_S锁是不冲突的。在InnoDB里有如下几种情况会请求S锁。

  1. 普通查询在隔离级别为 SERIALIZABLE 会给记录加 LOCK_S 锁。但这也取决于场景:非事务读(auto-commit)在 SERIALIZABLE 隔离级别下,无需加锁。
  2. 类似 SQL SELECT … IN SHARE MODE,会给记录加S锁,其他线程可以并发查询,但不能修改。基于不同的隔离级别,行为有所不同:  RC隔离级别:  LOCK_REC_NOT_GAP | LOCK_SRR隔离级别:如果查询条件为唯一索引且是唯一等值查询时,加的是 LOCK_REC_NOT_GAP | LOCK_S;对于非唯一条件查询,或者查询会扫描到多条记录时,加的是LOCK_ORDINARY | LOCK_S锁,也就是记录本身+记录之前的GAP;
  3. 通常INSERT操作是不加锁的,但如果在插入或更新记录时,检查到 duplicate key(或者有一个被标记删除的duplicate key),对于普通的INSERT/UPDATE,会加LOCK_S锁,而对于类似REPLACE INTO或者INSERT … ON DUPLICATE这样的SQL加的是X锁。而针对不同的索引类型也有所不同:   对于聚集索引(参阅函数row_ins_duplicate_error_in_clust),隔离级别小于等于RC时,加的是LOCK_REC_NOT_GAP类似的S或者X记录锁。否则加LOCK_ORDINARY类型的记录锁(NEXT-KEY LOCK);对于二级唯一索引,若检查到重复键,当前版本总是加 LOCK_ORDINARY 类型的记录锁(函数 row_ins_scan_sec_index_for_duplicate)。
  4. 外键检查 当我们删除一条父表上的记录时,需要去检查是否有引用约束(row_pd_check_references_constraints),这时候会扫描子表(dict_table_t::referenced_list)上对应的记录,并加上共享锁。按照实际情况又有所不同。我们举例说明

使用RC隔离级别,两张测试表:

 create table t1 (a int, b int, primary key(a));
 create table t2 (a int, b int, primary key (a), key(b), foreign key(b) references t1(a));
 insert into t1 values (1,2), (2,3), (3,4), (4,5), (5,6), (7,8), (10,11);
 insert into t2 values (1,2), (2,2), (4,4);

执行SQL:delete from t1 where a = 10;

  • 在t1表记录10上加 LOCKREC_NOT_GAP|LOCK_X
  • 在t2表的supremum记录(表示最大记录)上加 LOCK_ORDINARY|LOCK_S,即锁住(4, ~)区间

执行SQL:delete from t1 where a = 2;

  • 在t1表记录(2,3)上加 LOCK_REC_NOT_GAP|LOCK_X
  • 在t2表记录(1,2)上加 LOCK_REC_NOT_GAP|LOCK_S锁,这里检查到有引用约束,因此无需继续扫描(2,2)就可以退出检查,判定报错。

执行SQL:delete from t1 where a = 3;

  • 在t1表记录(3,4)上加 LOCK_REC_NOT_GAP|LOCK_X
  • 在t2表记录(4,4)上加 LOCK_GAP|LOCK_S

另外从代码里还可以看到,如果扫描到的记录被标记删除时,也会加LOCK_ORDINARY|LOCK_S 锁。具体参阅函数row_ins_check_foreign_constraint

5. INSERT … SELECT插入数据时,会对SELECT的表上扫描到的数据加LOCK_S锁

3.2 排他锁(lock_X )

    排他锁的目的主要是避免对同一条记录的并发修改。通常对于UPDATE或者DELETE操作,或者类似SELECT … FOR UPDATE操作,都会对记录加排他锁。  

create table t1 (a int, b int, c int, primary key(a), key(b));
insert into t1 values (1,2,3), (2,3,4),(3,4,5), (4,5,6),(5,6,7);

执行SQL(通过二级索引查询):update t1 set c = c +1 where b = 3;

  • RC隔离级别:1. 锁住二级索引记录,为NOT GAP X锁;2.锁住对应的聚集索引记录,也是NOT GAP X锁。
  • RR隔离级别下:1.锁住二级索引记录,为LOCK_ORDINARY|LOCK_X锁;2.锁住聚集索引记录,为NOT GAP X锁

执行SQL(通过聚集索引检索,更新二级索引数据):update t1 set b = b +1 where a = 2;

  • 对聚集索引记录加 LOCK_REC_NOT_GAP | LOCK_X锁;
  • 在标记删除二级索引时,检查二级索引记录上的锁(lock_sec_rec_modify_check_and_lock),如果存在和LOCK_X | LOCK_REC_NOT_GAP冲突的锁对象,则创建锁对象并返回等待错误码;否则无需创建锁对象;
  • 当到达这里时,我们已经持有了聚集索引上的排他锁,因此能保证别的线程不会来修改这条记录。(修改记录总是先聚集索引,再二级索引的顺序),即使不对二级索引加锁也没有关系。但如果已经有别的线程已经持有了二级索引上的记录锁,则需要等待。
  • 在标记删除后,需要插入更新后的二级索引记录时,依然要遵循插入意向锁的加锁原则。

我们考虑上述两种 SQL 的混合场景,一个是先锁住二级索引记录,再锁聚集索引;另一个是先锁聚集索引,再检查二级索引冲突,因此在这类并发更新场景下,可能会发生死锁。

四 表锁

     InnoDB的表级别锁包含五种锁模式:LOCK_IS、LOCK_IX、LOCK_X、LOCK_S以及LOCK_AUTO_INC锁,锁之间的相容性遵循数组lock_compatibility_matrix中的定义。
     InnoDB表级锁的目的是为了防止DDL和DML的并发问题。但从5.5版本开始引入MDL锁后,InnoDB层的表级锁的意义就没那么大了,MDL锁本身已经覆盖了其大部分功能。以下我们介绍下几种InnoDB表锁类型。

LOCK_IS/LOCK_IX

     也就是所谓的意向锁,这实际上可以理解为一种“暗示”未来需要什么样行级锁,IS表示未来可能需要在这个表的某些记录上加共享锁,IX表示未来可能需要在这个表的某些记录上加排他锁。意向锁是表级别的,IS和IX锁之间相互并不冲突,但与表级S/X锁冲突。
     在对记录加S锁或者X锁时,必须保证其在相同的表上有对应的意向锁或者锁强度更高的表级锁。

LOCK_X

当加了LOCK_X表级锁时,所有其他的表级锁请求都需要等待。通常有这么几种情况需要加X锁:

  • DDL操作的最后一个阶段(ha_innobase::commit_inlace_alter_table)对表上加LOCK_X锁,以确保没有别的事务持有表级锁。通常情况下Server层MDL锁已经能保证这一点了,在DDL的commit 阶段是加了排他的MDL锁的。但诸如外键检查或者刚从崩溃恢复的事务正在进行某些操作,这些操作都是直接InnoDB自治的,不走server层,也就无法通过MDL所保护;
  • 当设置会话的autocommit变量为OFF时,执行LOCK TABLE tbname WRITE这样的操作会加表级的LOCK_X锁(ha_innobase::external_lock);
  • 对某个表空间执行discard或者import操作时,需要加LOCK_X锁(ha_innobase::discard_or_import_tablespace)。

LOCK_S

  • 在DDL的第一个阶段,如果当前DDL不能通过ONLINE的方式执行,则对表加LOCK_S锁(prepare_inplace_alter_table_dict);

  • 设置会话的autocommit为OFF,执行LOCK TABLE tbname READ时,会加LOCK_S锁(ha_innobase::external_lock)。

从上面的描述我们可以看到LOCK_X及LOCK_S锁在实际的大部分负载中都很少会遇到。主要还是互相不冲突的LOCK_IS及LOCK_IX锁。

自增锁

第二章已经介绍了。不在重复。

下一节整理下关键代码。

参考:

https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html

http://mysql.taobao.org/monthly/2016/01/01/

猜你喜欢

转载自blog.csdn.net/bohu83/article/details/82765379