锁是计算机协调多个进程或线程并发访问某一资源的机制。为保证数据的一致性,需要对并发操作进行控制 ,因此产生了锁 。同时锁机制也为实现MySQL 的各个隔离级别提供了保证。 锁冲突也是影响数据库 并发访问性能的一个重要因素。
对于数据库中并发事务的读-读情况并不会引起什么问题。对于写-写、读-写或写-读这些情况可能会引起一些问题。为了满足读-读情况不受影响,又要使写-写、读-写 或写-读情况中的操作相互阻塞,MySQL实现了两种锁
- 读锁Lock_S,:称为共享锁 、也用表示S表示
- 写锁Lock_X :称为排他锁 、也用X表示
对于 InnoDB 引擎来说,读锁和写锁可以加在表上,也可以加在行上。兼容性如下:
MySQL的锁从锁粒度上可以分为全局锁、表锁、行级锁。全局锁一般开发中用的比较少。本文重点介绍表锁和行锁。其中,行级锁只在InnoDB 引擎实现。
1 全局锁
全局锁就是对整个数据库实例加锁,通常用于全局的逻辑备份。MySQL 提供了一个加全局读锁的命令
Flush tables with read lock
可以让整个库都处于只读状态 。这个命令在主库上执行,更新命令都被阻塞。在从库上,binlog不能执行。
mysqldump使用参数--single-transaction,启动一个事务,确保拿到一致性视图。而由于MVCC的支持,这个过程中数据是可以正常更新的
2 表级锁
2.1 MySQL Server 的表锁
MySQL Server 层的表锁一般在执行 DDL 时使用。最常见的是元数据锁(meta data lock,MDL)。MDL不需要显式使用:
- 当对⼀个表做增删改查操作的时候,默认加MDL读锁;
- 当对⼀个表结构做变更操作时,加MDL写锁;
- 事务提交后MDL锁⾃动释放。
也可以用以下命令显示的使用表锁:
lock tables t read; // 加读锁
lock tables t2 write; // 加写锁
unlock tables; // 解锁
2.2 InnoDB的表锁
InnoDB的表级别锁包含五种锁模式:LOCK_IS、LOCK_IX、LOCK_X、LOCK_S以及LOCK_AUTO_INC锁。
2.2.1 表级共享锁/排他锁
表级排他锁LOCK_X
当加了LOCK_X表级锁时,所有其他的表级锁请求都需要等待。加表级LOCK_X的场景:
- DDL操作的最后一个阶段,以确保没有别的事务持有表级锁
通常情况下,在DDL的commit 阶段是加了Server层的排他的MDL锁的。但诸如外键检查或者刚从崩溃恢复的事务正在进行某些操作,这些操作都是直接InnoDB,而不走server层。
- 当设置会话的autocommit变量为OFF时,执行LOCK TABLE t WRITE这样的操作
- 对某个表空间执行discard或者import操作
表级共享锁LOCK_S
加表级LOCK_S的场景:
在DDL的第一个阶段,如果当前DDL不能通过ONLINE的方式执行,则对表加LOCK_S锁
设置会话的autocommit为OFF,执行LOCK TABLE t READ时,会加LOCK_S锁
2.2.2 意向锁(LOCK_IS/LOCK_IX)
InnoDb 支持的意向锁为表级别的。
意向锁可以理解为一种“暗示”未来需要什么样行级锁,LOCK_IS表示未来可能需要在这个表的某些记录上加共享锁S,LOCK_IX表示未来可能需要在这个表的某些记录上加排他锁X。意向锁不会与行级的S / X互斥。
意向共享锁 (IS Lock) 和意向排他锁 (IX Lock)
- 意向共享锁,事务想获得一张表中某几行的共享锁;要获取某些行的 S 锁,必须先获得表的 IS 锁。
- 意向排他锁,事务想获得一张表中某几行的排他锁。要获取某些行的 X 锁,必须先获得表的 IX 锁。
意向锁是有数据引擎自己维护的,用户无法手动操作意向锁,在为行加共享锁S / 排他锁X之前,InooDB 会先获取该数据行所在在数据表的对应意向锁。
意向锁要解决的问题
以表user为例,考虑下列场景:
事务 A 获取了user表某一行的排他锁,并未提交:
事务 B 想要获取 users 表的表锁:
此时事务 B 检测事务 A 持有 users 表的意向排他锁,就可以得知事务 A 必然持有该表中某些数据行的排他锁,那么事务 B 对 users 表的加锁请求就会被排斥,而无需去检测表中的每一行数据是否存在排他锁。
意向锁兼容性
-
意向锁之间互相兼容
-
意向锁与表级别的排他锁/共享锁的兼容性
注意这里的排他锁和共享锁指的都是表锁,意向锁不会与行级的共享锁或排他锁互斥。
2.3 自增锁LOCK_AUTO_INC
自增锁是当向使用含有AUTO_INCREMENT列的表中插入数据时需要获取的一种特殊的表级锁,在执行插入语句时就在表级别加一个AUTO-INC锁,然后为每条待插入记录的AUTO_INCREMENT修饰的列分配递增的值,在该语句执行结束后,再把AUTO-INC锁释放掉。
3 行级锁
MySQL中只有InnoDB支持行级锁。行锁按照不同的实现方式,可以分为记录锁、间隙锁、临键锁。
3.1 间隙锁、临键锁、记录锁
3.1.1记录锁(LOCK_REC_NOT_GAP)
也叫行锁, 仅仅锁住索引记录的一行,在单条索引记录上加锁。
行级锁并不是直接锁记录,而是锁索引。即使该表上没有任何索引,也会创建一个隐藏的聚集主键索引。
- 如果一条sql语句操作了主键索引,MySQL就会锁定这条主键索引;
- 如果一条语句操作了非主键索引,MySQL会先锁定该非主键索引,再锁定相关的主键索引。
只有唯一索引且是唯一等值查询时,才有记录锁,锁定单条索引记录。记录锁是有S锁和X锁之分的,称之为 S型记录锁 和 X型记录锁 :
- 当获取了一条记录的S型记录锁后,其他事务也可记录的S型记录锁,但不可以获取X型记录锁;
- 当一个事务获取了一条记录的X型记录锁后,其他事务既不可以S型记录锁或X型记录锁。
3.1.2 间隙锁 (LOCK_GAP)
间隙锁只锁住一段范围,不锁记录本身,通常表示两个索引记录之间,或者索引上的第一条记录之前,或者最后一条记录之后的锁。是一种区间锁,
间隙锁存在于非唯一索引(包括非唯一索引的等值查询),或者范围查询中(包括唯一索引的范围查询),锁定开区间范围
注意
- 间隙锁在本质上是不区分共享或互斥的,而且间隙锁是不互斥
- 间隙锁只存在RR隔离级别下,Gap lock的作用时阻止多个事务将数据插入同一范围,导致phantom Problem问题。
在RU和RC两种隔离级别下,即使你使用select ... in share mode/ for update,也无法防止幻读。因为这两种隔离级别下只会有行锁,而不会有间隙锁。
3.1.3 临键锁(LOCK_ORDINARY ,Next-Key Lock)
临键锁,是记录锁与间隙锁的组合,它的封锁范围,包含记录本身及记录之前的GAP。是一个左开右闭区间。
在UPDATE、DELETE操作时,MySQL不仅锁定WHERE条件扫描过的所有索引记录,而且会锁定相邻的键值,即所谓的next-key locking。
间隙锁存在于非唯一索引(包括非唯一索引的等值查询),或者范围查询中(包括唯一索引的范围查询),锁定一段左开右闭的索引区间。
3.2 行级共享锁/排他锁
行锁也有读锁和写锁两种模式,即共享锁(LOCK_S)排他锁(LOCK_X)。通常共享锁排他锁不特殊说明(表级)默认指行级。
3.2.1 共享锁LOCK_S
共享锁的作用通常用于在事务中读取一条行记录后,不希望它被别的事务锁修改。如下几种情况会请求LOCK_S锁:
情况一:SELECT … IN SHARE MODE
基于不同的隔离级别,行为有所不同:
- RC隔离级别:LOCK_RECORD | LOCK_S
- RR隔离级别:如果查询条件为唯一索引且是唯一等值查询时, 加的是
LOCK_RECORD | LOCK_S;对于非唯一条件查询,或者查询会扫描到多条记录时, 加的是 Next-Key Lock | LOCK_S锁;
情况二:duplicate类型的INSERT:
通常普通的INSERT操作是不加锁的,但如果在插入或更新记录时,检查到 duplicate key(或者有一个被标记删除的duplicate key),会加LOCK_S锁(对于类似REPLACE INTO或者INSERT … ON DUPLICATE这样的SQL加的是X锁)。
针对不同的索引类型也有所不同
- 对于聚集索引
RC隔离级别:LOCK_RECORD | LOCK_S;
RR隔离级别:Next-Key Lock | LOCK_S - 对于二级唯一索引
当前版本总是加 Next-Key Lock 类型锁
情况三:普通查询在隔离级别为 SERIALIZABLE 会给记录加 LOCK_S 锁
3.2.2 排他锁 LOCK_X
排他锁的目的主要是避免对同一条记录的并发修改。通常对于UPDATE或者DELETE操作,或者类似SELECT … FOR UPDATE操作,都会对记录加LOCK_X。
情况一:通过二级索引查询
RC隔离级别:
锁住二级索引记录,为LOCK_REC_NOT_GAP | LOCK_X锁;
锁住对应的聚集索引记录,也是LOCK_RECORD | LOCK_X锁RR隔离级别下:
锁住二级索引记录,为Next-Key Lock| LOCK_X锁;
锁住聚集索引记录,为LOCK_RECORD | LOCK_X锁
情况二:通过聚集索引检索,更新二级索引数据
- 对聚集索引记录加 LOCK_REC_NOT_GAP | LOCK_X锁;
- 修改记录总是先聚集索引,再二级索引的顺序,即使不对二级索引加锁也没有关系。但如果已经有别的线程已经持有了二级索引上的记录锁,则需要等待
3.3 插入意向锁 (Insert Intention Lock)
插入意向锁是在插入一条记录行前,由 INSERT 操作产生的一种间隙锁。该锁用以表示插入意向,当多个事务在同一区间(gap)插入位置不同的多条数据时,事务之间不需要互相等待。
插入意向锁控制和解决并发插入
MySql InnoDB 在 Repeatable-Read 的事务隔离级别下,使用插入意向锁来控制和解决并发插入。
- 插入意向锁是一种特殊的间隙锁。
- 插入意向锁在锁定区间相同但记录行本身不冲突的情况下互不排斥。
假设存在两条值分别为 4 和 7 的记录,两个不同的事务分别试图插入值为 5 和 6 的两条记录,每个事务在获取插入行上独占的(排他)锁前,都会获取(4,7)之间的间隙锁,但是因为数据行之间并不冲突,所以两个事务之间并不会产生冲突(阻塞等待)
为什么不用间隙锁
因为事务B插入的记录6位于(4,7)区间内,而该区间内又存在一把间隙锁(插入记录5的事务A),而间隙锁的作用是阻止多个事务将数据插入同一范围 。所以事务B 只能等待 事务 A 结束,才能执行插入操作。这样做事务之间将会频发陷入阻塞等待,插入的并发性非常之差。