InnoDB Locks Set by Different SQL Statements
处理SQL语句时, Locking Read
, UPDATE
, 和DELETE
会在扫描过的索引记录上加record lock. InnoDB不会记住WHERE
条件, 只知道扫描了哪一部分索引. Lock通常指代next-key lock, 其会锁住一个索引记录和其之前的间隙.
若使用secondary index(二级索引)搜索, 且索引记录的锁为exclusive, InnoDB会锁住clustered index上对应的索引记录.
若未能找到匹配SQL语句的索引, MySQL必须扫描整个表, 并锁住表中的每一行, 导致其他用户无法写入任何数据, 因此最好为query创建合适的索引.
InnoDB的锁类型如下:
SELECT ... FROM
该语句为consistent read, 从数据库快照中读取, 且只要不为SERIALIZABLE
隔离级别, 就不会加任何锁. 若事务隔离级别为SERIALIZABLE
, 则在索引记录上加shared next-key lock
; 若使用unique index(唯一索引)搜索某一行, 则在对应的索引记录上加record lock
.SELECT ... FOR UPDATE
,SELECT ... FOR SHARE
上述语句使用唯一索引时, 会锁住扫描过的行, 并释放不匹配WHERE
条件的行上的锁. 然而在某些情况下, 由于结果行
与原始数据源
之间的关系丢失, 不会立即释放锁, 例如在UNION
中, 在判断被扫描的行是否符合条件之前, 这些行被插入到一个临时表. 此时, 临时表中的行与原始表中的行不存在任何关系, 原始表中的行直到查询结束之前都不会释放锁.Locking Read(
SELECT FOR UPDATE
,SELECT FOR SHARE
,UPDATE
,DELETE
)
上述语句的加锁规则分为两种情况:- 具有唯一搜索条件的唯一索引: InnoDB使用
record lock
锁住目标行对应的索引记录 - 范围搜索, 或非唯一索引: InnoDB使用
gap lock
或next-key lock
锁住扫描的索引记录, 以防止其他事务插入到间隙中
- 具有唯一搜索条件的唯一索引: InnoDB使用
对于扫描到的索引记录,
SELECT ... FOR UPDATE
会阻止其他事务执行SELECT ... FOR SHARE
, 并阻止其他事务在特定事务隔离级别下读取.UPDATE ... WHERE ...
该语句在扫描过的每个索引记录上添加exclusive next-key lock
; 若使用唯一索引搜索到唯一一行, 则降级为record lock
.当
UPDATE
修改clustered index
上的索引记录时, 会锁住受影响的secondary index(二级索引)上的记录.UPDATE
还会在两种情况下在secondary index的记录上加shared lock
:- 插入新的
secondary index
记录 - 插入新的
secondary index
记录前, 执行duplicate check scan
(重复检查扫描)
- 插入新的
DELETE FROM ... WHERE ...
该语句在每个扫描过的索引记录上添加exclusive next-key lock
; 若使用唯一索引搜索到唯一一行, 则降级为record lock
INSERT
在被插入的行上添加exclusive lock
. 该锁为record lock
, 而不是next-key lock
, 不会阻止其他事务插入到目标行的前后间隙
在插入行之前, 需先获得一种gap lock
, 称为insert intention gap lock
. 该锁表示当前事务有意插入, 若其他事务插入到同一间隙的不同位置, 则无需等待当前事务: 假设InnoDB中有两个索引记录, 值分别为4和7, 两个事务分别插入5和6, 在获得插入行的exclusive lock
前, 两个事务会使用insert intention lock
锁住(4,7)
间隙, 且不会互相阻塞, 因为插入值不同.
若发生duplicate-key error
(冲突键错误), 则会在重复的索引记录上添加shared lock
. 若多个事务尝试插入同一行, 而该行已存在, 且其他事务持有该行的exclusive lock
, 则shared lock
会导致死锁; 若其他事务删除该行, 也会发生这种情况. 假设InnoDB中table t1
的结构如下:
CREATE TABLE t1 (i INT, PRIMARY KEY (i)) ENGINE = InnoDB; |
假设三个事务按从上向下的顺序执行:
- 事务1:
START TRANSACTION;
INSERT INTO t1 VALUES(1); - 事务2:
START TRANSACTION;
INSERT INTO t1 VALUES(1); - 事务3:
START TRANSACTION;
INSERT INTO t1 VALUES(1); - 事务1:
ROLLBACK;
事务1获得该行的exclusive lock
, 事务2和3的插入操作导致重复键错误, 并获得该行的shared key
; 当事务1回滚时会释放exclusive lock
, 此时事务2和3都试图将自己的shared lock
升级为exclusive lock
, 并导致死锁: 事务2和事务3都不愿放弃自己的shared lock
, 因此两者都无法获得exclusive lock
.
若表中包含键值为1的行, 且三个事务按照以下顺序执行, 也会出现类似情况:
- Session 1:
START TRANSACTION;
DELETE FROM t1 WHERE i = 1; - Session 2:
START TRANSACTION;
INSERT INTO t1 VALUES(1); - Session 3:
START TRANSACTION;
INSERT INTO t1 VALUES(1); - Session 1:
COMMIT;
第一个事务获得目标行的exclusive lock
, 事务2和3的操作导致重复键错误, 并获得目标行的shared lock
. 当事务1提交时, 其释放拥有的exclusive lock
, 并导致死锁: 由于事务2和3都不愿释放自己的shared lock
, 因此无法获得exclusive lock
.
INSERT ... ON DUPLICATE KEY UPDATE
该语句与INSERT
不同, 当发生重复键错误时, 该语句会获得目标行的exclusive lock
, 而不是shared lock
. 不同键值类型会导致不同的锁类型:- 主键重复:
exclusive record lock
- 唯一键重复:
exclusive next-key lock
- 主键重复:
REPLACE
当没有唯一键冲突时, 该语句与INSERT
上锁行为相同; 当发生冲突时, 会在目标行上放置exclusive next-key lock
INSERT INTO T SELECT ... FROM S WHERE ...
该语句会在所有插入行上设置exclusive record lock
, 若事务隔离级别为READ COMMITTED
, 则InnoDB执行consistent read
(不设置任何锁); 否则, InnoDB会在扫描过的行上设置shared next-key lock
. 使用基于statement的二进制日志进行回滚恢复时, 每条SQL语句都必须按照与最初完全相同的方式执行, 因此必须设置锁.CREATE TABLE ... SELECT ...
分为两种上锁情况:REPEATABLE READ
隔离级别:SELECT
会给扫描过的索引记录设置shared next-key lock
READ COMMITTED
隔离级别: 与consistent read
相同, 不会设置任何锁
REPLACE INTO t SELECT ... FROM s WHERE ...
,UPDATE t ... WHERE col IN (SELECT ... FROM s ...)
SELECT
子句会在扫描过的索引记录上设置shared next-key lock
若table中有
FOREIGN KEY
约束, 任何需要检查约束条件的insert
,update
,delete
都需要设置shared record lock
.LOCK TABLES
会设置table lock
. 但这些锁由MySQL设置, 而不是InnoDB. 若innodb_table_locks = 1
且autocommit = 0
, 则InnoDB可知道table-level lock
, MySQL也可以知道row-level lock
; 否则InnoDB无法检测到table lock
相关的死锁. 并且, 由于MySQL不知道row-level lock
, 有可能在其他事务已设置row-level lock
的情况下, 为当前事务设置table-level lock
LOCK TABLES
会在innodb_table_locks = 1
时为每个table设置两个锁(IS和IX). 若不想设置table lock
, 可改为innodb_table_locks = 0
. MySQL 8.0后, 若执行LOCK TABLES ... WRITE
, 即使innodb_table_locks = 0
, 仍会为目标表上锁,LOCK TABLES ... READ
同理.所有InnoDB lock都会在事务提交或中止时释放, 因此, 若
autocommit = 1
(已开启自动提交), 则无需任何LOCK TABLES
, 因此每条语句都是一个单独事务, 语句执行完毕后立即释放锁.若
autocommit = 1
, 则不应在事务中使用LOCK TABLES
, 因为LOCK TABLES
会自动提交当前事务, 并开启新的事务