MySQL InnoDB 锁 学习笔记

所在文集:数据库


本文的内容参考了:

  • 架构师之路 - 挖坑,InnoDB的七种锁
  • 架构师之路 - 插入InnoDB自增列,居然是表锁?
  • 架构师之路 - InnoDB并发插入,居然使用意向锁?
  • 架构师之路 - InnoDB,select为啥会阻塞insert?
  • 架构师之路 - 别废话,各种SQL到底加了什么锁?
  • 架构师之路 - 超赞,InnoDB调试死锁的方法!

下面会涉及到 MySQL 数据库隔离级别和索引,请先参见:

  • MySQL 数据库隔离级别 脏读 VS 不可重读读 VS 幻读 VS 可串行化
  • MySQL 索引学习笔记

自增锁

MySQL InnoDB 默认的隔离级别为 RR,假设有数据表:
t(id AUTO_INCREMENT, name);
数据表中有数据:

1, shenjian
2, zhangsan
3, lisi

事务 A 先执行,还未提交:insert into t(name) values(xxx);
事务 B 后执行:insert into t(name) values(ooo);

问:事务B会不会被阻塞? 答案是会阻塞,分析如下:

  • 事务 A 先执行 insert,会得到一条 (4, xxx) 的记录,由于是自增列,InnoDB 会自动增长,注意此时事务并未提交;
  • 事务 B 后执行 insert,假设不会被阻塞,那会得到一条 (5, ooo) 的记录;

此时,并未有什么不妥,但如果,事务 A 继续 insertinsert into t(name) values(xxoo);会得到一条 (6, xxoo) 的记录。
事务 A 再 selectselect * from t where id>3; 得到的结果是:

4, xxx
6, xxoo

注意:不可能查询到 (5, ooo) 的记录,因为在 RR 的隔离级别下,不可能读取到还未提交事务生成的数据。
这对于事务 A 来说,就很奇怪了,对于 AUTO_INCREMENT 的列,连续插入了两条记录,一条是 4,接下来一条变成了 6,就像莫名其妙的幻影。

自增锁是一种特殊的表级别锁(table-level lock),专门针对事务插入 AUTO_INCREMENT 类型的列。最简单的情况,如果一个事务正在往表中插入记录,所有其他事务的插入必须等待,以便第一个事务插入的行,是连续的主键值。

共享/排他锁(Shared and Exclusive Locks)

共享锁(S锁)和排他锁(X锁)是行级别的锁(row-level locking)

  • 事务拿到某一行记录的共享S锁,才可以读取这一行;
    • 多个事务可以拿到一把共享S锁,读读可以并行
  • 事务拿到某一行记录的排它X锁,才可以修改或者删除这一行;
    • 只有一个事务可以拿到排它X锁,写写/读写必须互斥;

共享/排它锁的潜在问题是,不能充分的并行,解决思路是数据多版本。参见:MySQL InnoDB 并发控制,事务的实现 学习笔记

意向锁(Intention Locks)

意向锁是指,未来的某个时刻,事务可能要加共享/排它锁了,先提前声明一个意向。意向锁有这样一些特点:

  • 首先,意向锁,是一个表级别的锁(table-level locking)
  • 意向锁分为:
    • 意向共享锁(intention shared lock, IS)例如:select ... lock in share mode 要设置意向共享锁;
    • 意向排它锁(intention exclusive lock, IX)例如:select ... for update 要设置意向排它锁;
  • 意向锁协议(intention locking protocol)并不复杂:
    • 事务要获得某些行的 S 锁,必须先获得表的 IS 锁
    • 事务要获得某些行的 X 锁,必须先获得表的 IX 锁
  • 由于意向锁仅仅表明意向,它其实是比较弱的锁,意向锁之间并不相互互斥,而是可以并行

插入意向锁(Insert Intention Locks)

对已有数据行的修改 update 与删除 delete,必须加排他锁(X锁),那对于数据的插入 insert,是否还需要加这么强的锁,来实施互斥呢?

插入意向锁,是间隙锁(Gap Locks)的一种(所以,也是实施在索引上的),它是专门针对 insert 操作的:
多个事务,在同一个索引,同一个范围区间插入记录时,如果插入的位置不冲突,不会阻塞彼此。

回到上面自增锁时所用的插入的例子。如果主键不是自增的 t(id unique PK, name); 数据表中有数据:

10, shenjian
20, zhangsan
30, lisi

事务 A 先执行,在 10 与 20 两条记录中插入了一行,还未提交:insert into t values(11, xxx);
事务 B 后执行,也在 10 与 20 两条记录中插入了一行:insert into t values(12, ooo);

问:会使用什么锁?使用的是插入意向锁。

问:事务B会不会被阻塞呢?虽然事务隔离级别是 RR,虽然是同一个索引,虽然是同一个区间,但插入的记录并不冲突,故这里并不会阻塞事务 B。

思路总结:

  • InnoDB 使用共享锁,可以提高读读并发;
  • 为了保证数据强一致,InnoDB 使用强互斥锁,保证同一行记录修改与删除的串行性;
  • InnoDB 使用插入意向锁,可以提高插入并发;

记录锁(Record Locks)

记录锁封锁索引记录,例如:
select * from t where id=1 for update;
它会在 id=1 的索引记录上加锁,以阻止其他事务插入,更新,删除 id=1 的这一行。

需要说明的是:
select * from t where id=1;
则是快照读(SnapShot Read),它并不加锁。

间隙锁(Gap Locks)

间隙锁封锁索引记录中的间隔,或者第一条索引记录之前的范围,又或者最后一条索引记录之后的范围。
t(id PK, name KEY); 表中有四条记录:

1, shenjian
3, zhangsan
5, lisi
9, wangwu

SQL 语句:select * from t where id between 8 and 15 for update;会封锁区间,以阻止其他事务 id=10 的记录插入。

为什么要阻止 id=10 的记录插入?如果能够插入成功,头一个事务执行相同的 SQL 语句,会发现结果集多出了一条记录,即幻影数据。(即我们采用 MySQL InnoDB 的 可重复读 RR 隔离级别)

如果把事务的隔离级别降级为读提交(Read Committed, RC),间隙锁则会自动失效。

临键锁(Next-Key Locks)

临键锁,是记录锁与间隙锁的组合,它的封锁范围,既包含索引记录,又包含索引区间。
更具体的,临键锁会封锁索引记录本身,以及索引记录之前的区间。
t(id PK, name KEY); 表中有四条记录:

1, shenjian
3, zhangsan
5, lisi
9, wangwu

主键上潜在的临键锁为:

(-infinity, 1]
(1, 3]
(3, 5]
(5, 9]
(9, +infinity]

临键锁的主要目的,也是为了避免幻读(Phantom Read)。如果把事务的隔离级别降级为RC,临键锁则也会失效。

各种SQL到底加了什么锁

普通 select

  • 在读未提交(Read Uncommitted),读提交(Read Committed, RC),可重复读(Repeated Read, RR)这三种事务隔离级别下,普通 selec 使用快照读(snpashot read),不加锁,并发非常高;
  • 在串行化(Serializable)这种事务的隔离级别下,普通 select 会升级为 select ... in share mode;

加锁 select

  • select ... for update
  • select ... in share mode

如果,在唯一索引(unique index)上使用唯一的查询条件(unique search condition),会使用记录锁(record lock),而不会封锁记录之间的间隔,即不会使用间隙锁(gap lock)与临键锁(next-key lock);

假设有 InnoDB 表:t(id PK, name);

1, shenjian
2, zhangsan
3, lisi

SQL 语句:select * from t where id=1 for update; 只会封锁记录,而不会封锁区间。
其他的查询条件和索引条件,InnoDB 会封锁被扫描的索引范围,并使用间隙锁与临键锁,避免索引范围区间插入记录。

update 与 delete

和加锁 select 类似,如果在唯一索引上使用唯一的查询条件来 update/delete,例如:
update t set name=xxx where id=1; 也只加记录锁;

否则,符合查询条件的索引记录之前,都会加排他临键锁(exclusive next-key lock),来封锁索引记录与之前的区间;

尤其需要特殊说明的是,如果 update 的是聚集索引(clustered index)记录,则对应的普通索引(secondary index)记录也会被隐式加锁,这是由 InnoDB 索引的实现机制决定的:普通索引存储 PK 的值,检索普通索引本质上要二次扫描聚集索引。

insert

同样是写操作,insertupdatedelete 不同,它会用排它锁封锁被插入的索引记录,而不会封锁记录之前的范围。

同时,会在插入区间加插入意向锁(insert intention lock),但这个并不会真正封锁区间,也不会阻止相同区间的不同 KEY 插入。

InnoDB 调试死锁

InnoDB 的行锁都是实现在索引上的,实验可以使用主键,建表时设定为 InnoDB 引擎:

create table t (
id int(10) primary key
)engine=innodb;

插入一些实验数据:

start transaction;
insert into t values(1);
insert into t values(3);
insert into t values(10);
commit;

实验一,间隙锁互斥

开启区间锁,RR 的隔离级别下,上例会有四个区间:

(-infinity, 1)
(1, 3)
(3, 10)
(10, infinity)

事务 A 删除某个区间内的一条不存在记录,获取到共享间隙锁,会阻止其他事务 B 在相应的区间插入数据,因为插入需要获取排他间隙锁。

session A:

set session autocommit=0;
start transaction;
delete from t where id=5;

session B:

set session autocommit=0;
start transaction;
insert into t values(0);
insert into t values(2);
insert into t values(12);
insert into t values(7);

事务 B 插入的值:0, 2, 12 都不在 (3, 10) 区间内,能够成功插入,而 7(3, 10) 这个区间内,会阻塞。

可以使用:show engine innodb status; 来查看锁的情况。

如果事务 A 提交或者回滚,事务 B 就能够获得相应的锁,以继续执行。
如果事务 A 一直不提交,事务 B 会一直等待,直到超时。

实验二,共享排他锁死锁

事务 A 先执行:

set session autocommit=0;
start transaction;
insert into t values(7);

事务 B 后执行:

set session autocommit=0;
start transaction;
insert into t values(7);

事务 C 最后执行:

set session autocommit=0;
start transaction;
insert into t values(7);

三个事务都试图往表中插入一条为 7 的记录:

  • A 先执行,插入成功,并获取 id=7 的排他锁;
  • B 后执行,需要进行PK校验,故需要先获取 id=7 的共享锁,阻塞;
  • C 后执行,也需要进行PK校验,也要先获取 id=7 的共享锁,也阻塞;

如果此时,事务 A 执行:rollback; 释放 id=7排他锁。
则 B,C 会继续进行主键校验:

  • B 会获取到 id=7共享锁,主键未互斥;
  • C 也会获取到 id=7 共享锁,主键未互斥;

B 和 C 要想插入成功,必须获得 id=7 的排他锁,但由于双方都已经获取到 id=7 的共享锁,它们都无法获取到彼此的排他锁,死锁就出现了。

当然,InnoDB有死锁检测机制,B 和 C 中的一个事务会插入成功,另一个事务会自动放弃。

共享排他锁,在并发量插入相同记录的情况下会出现,相应的案例比较容易分析。

实验三,并发间隙锁的死锁

SQL 执行序列如下:

A:set session autocommit=0;
A:start transaction;
A:delete from t where id=6;
         B:set session autocommit=0;
         B:start transaction;
         B:delete from t where id=7;
A:insert into t values(5);
         B:insert into t values(8);
  • A 执行 delete 后,会获得 (3, 10) 的共享间隙锁。
  • B 执行 delete 后,也会获得 (3, 10) 的共享间隙锁。
  • A 执行 insert 后,希望获得 (3, 10) 的排他间隙锁,于是会阻塞。
  • B 执行 insert 后,也希望获得 (3, 10) 的排他间隙锁,于是死锁出现。

检测到死锁后,事务 B 自动回滚了,事务 A 将会执行成功。

你可能感兴趣的:(MySQL InnoDB 锁 学习笔记)