最近跟速运联调系统,没顾上写笔记。之前看沈剑老师的公众号:架构师之路 有对锁做了介绍。还是参照官方文档整理下,有助于系统理解。主要是mysql.taobao的月报。
锁机制是数据库区别于文件系统的主要标志之一,用于管理对共享资源的并发访问。MYSQL支持表锁,innodb引擎支持行锁,主要分为基本概念,目的是了解锁类型,加锁场景。case没有补充。代码以mysql 5.7.17版本。
先看官网介绍:
This section describes lock types used by InnoDB
.
Shared and Exclusive Locks
Intention Locks
Record Locks
Gap Locks
Next-Key Locks
Insert Intention Locks
AUTO-INC Locks
Predicate Locks for Spatial Indexes
比常见的说法七种锁还多一种,最后一种看官网介绍。
InnoDB
implements standard row-level locking where there are two types of locks, shared (S
) locks and exclusive (X
) locks.
A 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锁。
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
类型的锁)。
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锁。
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,就会看到新插入的记录。
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上有意向锁),而不需要再去行级别判断。
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里可能有多个申请插入意向锁的会话。
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模式复制时,可能会影响到复制的数据一致性,官方文档 有详细描述.
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。
共享锁的作用通常用于在事务中读取一条行记录后,不希望它被别的事务锁修改,但所有的读请求产生的LOCK_S锁是不冲突的。在InnoDB里有如下几种情况会请求S锁。
LOCK_REC_NOT_GAP | LOCK_S
;RR隔离级别:如果查询条件为唯一索引且是唯一等值查询时,加的是 LOCK_REC_NOT_GAP | LOCK_S
;对于非唯一条件查询,或者查询会扫描到多条记录时,加的是LOCK_ORDINARY | LOCK_S
锁,也就是记录本身+记录之前的GAP;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;
LOCKREC_NOT_GAP|LOCK_X
LOCK_ORDINARY|LOCK_S
,即锁住(4, ~)区间执行SQL:delete from t1 where a = 2;
LOCK_REC_NOT_GAP|LOCK_X
LOCK_REC_NOT_GAP|LOCK_S
锁,这里检查到有引用约束,因此无需继续扫描(2,2)就可以退出检查,判定报错。执行SQL:delete from t1 where a = 3;
LOCK_REC_NOT_GAP|LOCK_X
LOCK_GAP|LOCK_S
锁另外从代码里还可以看到,如果扫描到的记录被标记删除时,也会加LOCK_ORDINARY|LOCK_S
锁。具体参阅函数row_ins_check_foreign_constraint
5. INSERT … SELECT插入数据时,会对SELECT的表上扫描到的数据加LOCK_S锁
排他锁的目的主要是避免对同一条记录的并发修改。通常对于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;
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表锁类型。
也就是所谓的意向锁,这实际上可以理解为一种“暗示”未来需要什么样行级锁,IS表示未来可能需要在这个表的某些记录上加共享锁,IX表示未来可能需要在这个表的某些记录上加排他锁。意向锁是表级别的,IS和IX锁之间相互并不冲突,但与表级S/X锁冲突。
在对记录加S锁或者X锁时,必须保证其在相同的表上有对应的意向锁或者锁强度更高的表级锁。
当加了LOCK_X表级锁时,所有其他的表级锁请求都需要等待。通常有这么几种情况需要加X锁:
ha_innobase::commit_inlace_alter_table
)对表上加LOCK_X锁,以确保没有别的事务持有表级锁。通常情况下Server层MDL锁已经能保证这一点了,在DDL的commit 阶段是加了排他的MDL锁的。但诸如外键检查或者刚从崩溃恢复的事务正在进行某些操作,这些操作都是直接InnoDB自治的,不走server层,也就无法通过MDL所保护;LOCK TABLE tbname WRITE
这样的操作会加表级的LOCK_X锁(ha_innobase::external_lock
);ha_innobase::discard_or_import_tablespace
)。在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/