MySQL InnoDB 锁 学习笔记

所在文集:数据库


本文的内容参考了:

下面会涉及到 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 将会执行成功。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335