目录
1.7 插入意向锁 - Insert Intention Locks
一.锁
实现事务隔离级别的过程中用到了锁,所谓锁就是在事务A修改某些数据时,对这些数据加一把锁,防止其他事务同时对这些数据执行修改操作;
当事务A完成修改操作后,释放当前持有的锁,以便其他事务再次上锁执行对应的操作。
不同存储引擎中的锁功能并不相同,这里我们重点介绍InnoDB存储引擎中的锁。
1.1 锁信息
锁的信息包括锁的请求(申请),锁的持有以及阻塞状态等等,都保存在 performance_schema 库的 data_locks 表中,可以通过以下方式查看:
SELECT * FROM performance_schema.data_locks\G
我们直接去查询的话,就会是空的
我们必须开启事务
start transaction;
然后我们必须执行一个DML语句,但是我们不提交,这个锁就会一直在当前事务
我们现在再去查询,就能查询到锁的信息了
SELECT * FROM performance_schema.data_locks\G
锁类型
锁类型依赖于存储引擎,在InnoDB存储引擎中按照锁的粒度分为,行级锁 RECORD 和表级锁 TABLE:
-
行级锁也叫行锁,是对表中的某些具体的数据行进行加锁;
-
表级锁也叫表锁,是对整个数据表进行加锁。
注意:MySQL8.0中没有页级锁
锁模式
锁模式,用来描述如何请求(申请)锁,分为共享锁(S)、独占锁(X)、意向共享锁(IS)、意向独占锁(IX)、记录锁、间隙锁、Next-Key锁、AUTO-INC 锁、空间索引的谓词锁等
这里介绍的锁类型和锁模式,也就是大家经常听过的锁分类
1.2 共享锁和独占锁 - Shared and Exclusive Locks
InnoDB实现了标准的行级锁,分为两种分别是共享锁(S锁)和独占锁(X锁),独占锁也称为排他锁。
想象你和其他人(代表不同的事务)正在共同管理一个非常重要的共享文档(代表数据库表中的一行数据)。为了保证数据准确,大家不能同时随意修改同一行,这时就需要“锁”这种机制来协调。
-
共享锁 (S锁) - 阅读许可证
-
作用: 它就像是获得了一份只读权限的许可证。当你拿到这个许可证(S锁)时,你的主要目的是安全地阅读这一行文档的内容,但是你不能修改这行文档的内容。
-
允许多少人: 这种许可证最大的特点是允许多人同时持有。也就是说,你拿着它在读这一行的时候,其他事务也可以过来申请并获得同样的“阅读许可证”(S锁),然后和你一起同时阅读这一行的内容。
-
限制:
-
只能读,不能改: 持有 S 锁的人(事务)只能看文档内容,不能对其进行任何修改、删除或添加操作。
-
阻止修改者: 最关键的是,只要有人(任何事务)持有这一行的 S 锁,就绝对不允许任何人(包括持有 S 锁的事务自己)获得修改权限(即 X 锁)。如果有人想修改这行数据,它必须等到所有持有该行 S 锁的人都释放了他们的许可证(事务结束或明确释放锁)之后才行。
-
-
场景: 当你执行一个类似
SELECT ... LOCK IN SHARE MODE
的查询时,InnoDB 就会给你查询结果集中的那些行加上 S 锁。这通常用在你想确保在读取数据之后、基于这个数据做决定之前,别人不会修改掉这些数据的情况(虽然你自己也不能改)。
-
-
独占锁 (X锁) - 独家编辑权
-
作用: 这相当于获得了对那一行文档的独家编辑权。当你拿到这个权限(X锁)时,你不仅可以看这行文档,更重要的是可以修改、删除它,或者完全重写它。
-
允许多少人: 这种权限是排他的、独占的。同一时间,只能有一个人(一个事务)获得这一行数据的 X 锁。只要你还拿着这个编辑权,其他人(其他事务)无论想干什么都不行。
-
限制:
-
阻止一切他人操作: 在你持有 X 锁期间:
-
其他事务不能获得 S 锁来读取这一行(因为你想修改时,可能不想让别人看到你修改了一半的数据,或者基于旧数据做决策)。
-
其他事务更不能获得 X 锁来修改同一行(同一时间只能一个人改)。
-
-
唯一权限: 你是唯一一个既能读又能写这行数据的人。
-
-
场景: 最常见的场景就是当你执行
UPDATE
或DELETE
语句修改或删除某一行数据时,InnoDB 会自动地、隐式地为你要修改或删除的那一行(或多行)加上 X 锁。这是为了保证在你修改的过程中,绝对不会有其他人(事务)来干扰你(无论是读还是写),从而确保数据修改的完整性和一致性。
-
总结一下关键区别:
-
共享锁 (S): “大家一起看,谁也别改”。允许多个读者同时存在,但完全阻止任何写操作(包括读者自己)。
-
独占锁 (X): “我的地盘我做主,你们都别动”。只允许一个操作者(既能读又能写)存在,彻底阻止其他任何人的读或写操作。
锁的冲突(通俗版):
-
如果一行上已经有人拿了 S 锁:
-
其他人再来申请 S 锁:可以,大家一起读。
-
其他人再来申请 X 锁:绝对不行,必须等所有 S 锁都释放完。
-
-
如果一行上已经有人拿了 X 锁:
-
其他人再来申请 S 锁:不行,必须等 X 锁释放。
-
其他人再来申请 X 锁:更不行,必须等当前的 X 锁释放。
-
-
根本原则: X 锁和任何其他锁(无论是 S 还是 X)都水火不容。 S 锁之间可以和平共处,但 S 锁和 X 锁不能共存。
对查询结果集中的每行数据都加共享锁
select * from account where id < 2 FOR SHARE; -- MySQL8.0 (推荐)
select * from account where id < 2 LOCK IN SHARE MODE; -- 8.0以及之前版本
对查询结果集中的每行数据都加排他锁
select * from account where id = 1 FOR UPDATE;
一般来说,我们是
- 开启事务
- 先在查询语句上加上相应的锁
- 执行DML操作
- 提交或者回滚事务自动释放锁
我们看个例子
现在我们去看一下锁的状态
SELECT * FROM performance_schema.data_locks\G
我们现在再加一个排他锁
我们再看看表的信息
接下来我换一个客户端,来开启另外一个事务看看
我们发现它阻塞在这里了。
我们回到事务1,回滚事务。事务结束,自动释放锁。
这个时候我们回到事务2再去看看
它修改成功了。
也就是说
如果事务 T1 持有行 R 上的共享锁 (S 锁):
-
事务 T2 请求 S 锁: 会立即被授予。此时,T1 和 T2 都持有行 R 的 S 锁。
-
事务 T2 请求 X 锁: 不能立即被授予,T2 将被阻塞,直到 T1 释放其在行 R 上的锁。
如果事务 T1 持有行 R 上的独占锁 (X 锁):
-
事务 T2 请求行 R 上的 S 锁或 X 锁: 都不能立即被授予。无论 T2 请求哪种锁,它都必须等待事务 T1 释放行 R 上的 X 锁。
读锁是共享锁的一种实现,写锁是排他锁的一种实现。
1.3 意向锁 - Intention Locks
InnoDB 支持多粒度锁机制,允许行级锁和表级锁共存。为了实现这种多粒度锁定,InnoDB 使用了意向锁。
意向锁是表级锁,但是它不是真正意义上的加锁,它并不直接锁定表中的数据行。它更像是一种声明,记录在系统数据结构中,表明一个事务打算(意向) 对表中的哪些行施加哪种类型的锁(共享锁 S 或排他锁 X)。
意向锁分为两种:
-
意向共享锁 (IS): 表示事务计划对表中的某些行施加共享锁 (S 锁)。
-
意向排他锁 (IX): 表示事务计划对表中的某些行施加排他锁 (X 锁)。
在获取行锁时,必须遵循以下协议:
-
事务在获取表中某一行的共享锁 (S) 之前,必须先获得该表上的 IS 锁(或强度更高的锁)。
-
事务在获取表中某一行的排他锁 (X) 之前,必须先获得该表上的 IX 锁。
这怎么理解?
想象一个场景:图书馆(数据库表)和书架上的书(数据行)
你想看书(读操作):
你(事务)走到某个书架前,打算拿一本特定的书(某一行)来读。
但图书馆管理员(InnoDB)有个规定:在你真正伸手去拿那本书之前,你必须先在图书馆入口的登记簿(表级意向锁)上做个标记。
你在登记簿上写:“我打算读这个书架上的某本书(IS - 意向共享锁)”。这个标记不是说你把整本书架锁住了不让别人碰,它只是告诉管理员:“我接下来可能会拿这本书架上的某一本书来读,请留意一下”。
你想借走或修改书(写操作):
你(事务)走到某个书架前,打算拿一本特定的书(某一行)来借走(删除)或者在书上做笔记(更新)。
同样,规定要求:在你真正伸手去拿那本书之前,你必须先在图书馆入口的登记簿(表级意向锁)上做个标记。
你在登记簿上写:“我打算借走或修改这个书架上的某本书(IX - 意向排他锁)”。这个标记不是说你把整本书架锁住了不让别人靠近,它只是告诉管理员:“我接下来可能会拿这本书架上的某一本书来修改或借走,请特别留意!”。
意向锁的主要优势在于提升锁管理效率: 在真正对某一行加锁之前,数据库不需要遍历检查表中每一行是否已被锁定。只需检查表上已有的意向锁类型,即可快速判断该意向锁是否与即将施加的行锁意图兼容。
我知道大家可能好奇,为什么可以提升锁管理效率?
为什么需要这个“登记簿”(意向锁)?
避免全馆大检查(提升效率): 想象一下,如果没有这个登记簿。管理员(InnoDB)想知道现在有没有人在看书架上的某本书(行级锁),或者有没有人想借书(行级锁),它就得跑到书架前,一本一本书地仔细检查每本书是否被拿在手里(是否有行锁)。这非常慢,尤其对于大图书馆(大表)!
锁的授予规则如下:
-
当新事务请求一个锁时,如果该锁与表上以及目标行上现有的所有锁兼容,则该锁会被立即授予。
-
如果请求的锁与表上或目标行上现有的某个锁冲突,则该锁不会被立即授予。请求事务将进入阻塞等待状态,直到与其请求冲突的所有锁被释放。
意向锁与行级锁的兼容性如下表:
简单记忆:
-
X
锁: 谁都不让进(全冲突)。 -
IX
锁: 不介意其他打算写的人(IX
)和打算读的人(IS
),但讨厌读(S
)或写(X
)的人。 -
S
锁: 欢迎其他读者(S
,IS
),讨厌任何想写的人(IX
,X
)。 -
IS
锁: 欢迎其他读者(S
,IS
)和打算写的人(IX
),只讨厌写(X
)的人。
除了全表锁定请求之外,意向锁不会阻止任何锁请求;
意向锁的主要目的是表示事务正在锁定某行或者正在意图锁定某行。
1.4 索引记录锁 - Record Locks
索引记录锁或称为精准行锁,顾名思意是指索引记录上的锁,如下SQL锁住的是指定的一行:
-- 防止任何其他事务插入、更新或删除值为1的行,id为索引列
SELECT * FROM account WHERE id = 1 For UPDATE;
索引记录锁总是锁定索引行,在表没有定义索引的情况下,InnoDB创建一个隐藏的聚集索引,并使用该索引进行记录锁定,当使用索引进行查找时,锁定的只是满足条件的行,如图所示:
我知道大家一脸懵逼,还没搞清楚:
想象一个巨大的图书馆(数据库表),里面有几百万本书(数据行)。
-
图书馆的“目录卡”系统(索引):
-
图书馆有专门的 目录室,里面存放着成千上万张 目录卡片(索引条目)。
-
每张卡片对应 一本具体的书(一行数据)。
-
卡片按特定规则排序存放:
-
按书名排序(主键索引):卡片盒A里的卡片按书名A-Z排列。
-
按作者排序(二级索引):卡片盒B里的卡片按作者名字A-Z排列。
-
按主题排序(二级索引):卡片盒C里的卡片按主题分类排列。
-
-
每张卡片上记录了 书名、作者、主题 等关键信息,最重要的是 这本书在图书馆哪个书架哪一层(行数据的物理位置)。
-
-
“索引记录锁”是什么?
-
当你想 预定(锁定) 图书馆里的某本书(数据行),不让别人在你使用期间拿走或修改时,你不是直接跑到书架上在那本书上贴个“已预定”的标签(虽然最终效果是这样)。
-
你 必须先到目录室,找到代表那本书的 目录卡片(索引条目)。
-
然后,你在 这张目录卡片上贴一个“已预定”的标签(加索引记录锁)。
-
这个“标签”的作用:
-
告诉所有人: “这本书被我预定了,其他人不能预定它或修改它代表的书的信息!”
-
阻止其他人: 如果有人也想预定同一本书,他们必须先来目录室查看这张卡片。看到卡片上有“已预定”标签,他们就知道这本书已被占用,必须等待你用完释放标签(解锁)才能预定。
-
精确定位: 通过锁卡片,你精确地锁定了那一本特定的书。其他书(即使在同一书架)不受影响。
-
-
-
为什么锁卡片(索引)而不是直接锁书(数据行)?
-
效率高: 目录室里的卡片是按规则有序排列的,查找特定卡片非常快。想象一下,如果没有目录卡,你要在几百万本书的书架上找到你要的那本书来贴标签,得花多少时间!锁索引就是利用了索引的有序性和快速查找能力。
-
管理方便: 所有“预定”信息都集中在目录室(索引结构)里管理。管理员(数据库)要检查一本书是否被预定,只需去目录室看对应的卡片有没有标签,不需要跑遍所有书架。
-
避免混乱: 如果直接在书上贴标签,书可能被移动、整理,标签容易丢失或混乱。目录卡片是固定的、稳定的参照点。索引结构相对稳定,是锁定的理想锚点。
-
-
没有索引怎么办?隐藏的“馆藏编号”系统(聚集索引):
-
想象一个非常老旧的图书馆,没有按书名、作者、主题分类的目录卡(没有用户定义的索引)。这似乎没法快速找到书和贴标签了?
-
InnoDB的聪明之处: 它内部维护了一个 隐藏的“馆藏编号”系统(隐藏的聚集索引)。
-
每本书都有一个 唯一的、看不见的内部编号(通常是
ROW_ID
)。这个编号通常按照书入库上架的 物理顺序 生成。 -
图书馆管理员(InnoDB)手里有一份 秘密清单(隐藏的B+树索引),记录了 内部编号 -> 书架位置 的映射关系。这份清单是按内部编号排序的。
-
当你想预定某本书(如:书名是《三体》),但没有书名索引时:
-
管理员(InnoDB)必须拿着这份秘密清单,从编号最小的书开始,一本一本去书架上核对书名(全表扫描)。
-
当他 终于找到《三体》这本书时:
-
他会查看这本书对应的 内部编号。
-
然后回到他的秘密清单(隐藏索引),找到 代表这本《三体》的那一行记录(索引条目)。
-
在这行记录上贴上“已预定”的标签(加索引记录锁)!
-
-
即使没有用户定义的索引,锁定动作 最终还是发生在这个隐藏索引的条目上。锁定的仍然是 索引记录(这里是隐藏聚集索引的记录),而不是漫无目的地在物理书上贴标签。
-
-
-
“锁定的只是满足条件的行”是什么意思?
-
继续用找《三体》的例子。
-
管理员(InnoDB)拿着秘密清单(隐藏索引)一本本核对书名(扫描每一行)。
-
当他核对 《平凡的世界》 时:
-
书名不是《三体》? 不满足条件。
-
他 不会 在《平凡的世界》对应的秘密清单条目上贴“已预定”标签!跳过,不锁定这行。
-
-
当他核对 《三体》 时:
-
书名匹配! 满足条件。
-
他 立即 在《三体》对应的秘密清单条目上贴“已预定”标签(加锁)。
-
-
所以,即使他扫描了成千上万本书(行),他 最终只在真正满足你查询条件(书名=《三体》)的那本书(行)对应的索引条目上加了锁。其他不满足条件的书(行)完全不受影响,别人可以自由预定或修改它们。
-
总结核心要点:
-
索引记录锁的本质: 就是在 代表数据行的索引条目上加锁。无论索引是用户定义的(如书名索引)还是InnoDB自动创建的隐藏聚集索引(内部编号索引)。
-
为什么锁索引: 因为索引是 快速定位数据行的入口,在入口处加锁效率最高、管理最方便、冲突最清晰。锁定了索引条目,就等同于锁定了它指向的那一行数据。
-
无索引怎么办: InnoDB 一定会使用隐藏的聚集索引(基于内部ROW_ID)。锁定动作最终仍然是作用在这个隐藏索引对应的条目上。
-
“只锁满足条件的行”: 即使需要全表扫描(一本本书翻),InnoDB也会 一行行检查。只有 当前检查的行完全符合你的查询条件(如 WHERE name='三体')时,它才会 在那个特定行对应的索引条目上加锁。检查过程中遇到的不符合条件的行,瞬间就放过了,完全不加锁。扫描结束后,锁只留在目标行上。
1.5 间隙锁 - Gap Locks
什么是间隙?
要理解间隙锁,就得知道间隙是什么?
核心:什么是“间隙”?
-
想象土地(数据): 你管理的土地被划分成许多小块(数据行),每块地都有一个唯一且按顺序排列的门牌号(这对应于数据库表上的索引,比如主键
id
)。假设现有的地块门牌号是:100
,200
,300
,500
,800
。 -
“间隙”就是门牌号之间的“空白地带”:
-
在
100
号和200
号地块之间,存在着一个间隙。这个间隙包含了所有大于100号且小于200号的理论上门牌号位置,比如101
,102
, ...150
, ...199
。现实中,这些位置是空地,没有任何建筑物(数据行)。 -
在
200
号和300
号地块之间,存在另一个间隙(200,300
)——>开区间,不包含200和300。 -
在
300
号和500
号地块之间,存在一个更大的间隙(300,500
)——>开区间,不包含300和500。 -
在
500
号和800
号地块之间,存在间隙(500,800
)——>开区间,不包含500和800。
-
-
边界外的“无限间隙”:
-
最小地块之前: 在最小的地块(
100
号)之前,也存在一个间隙。它包含了所有小于100号的门牌号位置(比如1
,2
, ...99
),直到理论上的“负无穷”。我们记作(-∞, 100)
。 -
最大地块之后: 在最大的地块(
800
号)之后,也存在一个间隙。它包含了所有大于800号的门牌号位置(801
,802
, ...),直到理论上的“正无穷”。我们记作(800, +∞)
。
-
什么是间隙锁?
对于间隙锁(Gap Lock),抓住核心,抛开技术细节,专注于它的本质行为和目的。
想象你是一个土地规划员(数据库)。
-
你的任务: 你要规划一片巨大的、连续的土地(数据库表中的数据)。这片土地被划分成很多小块(数据行),每块地都有唯一的门牌号(主键或索引值),比如 100号、200号、300号... 一直到 900号。有些地块目前是空的(间隙)。
-
重要事务: 现在,你需要为一个重要项目(一个数据库事务)圈定一个范围的土地,比如门牌号从 200号 到 800号 的所有地块。你需要确保在这个项目完成(事务提交)之前,这个范围内的状况是稳定不变的——你不仅要考察现在已有的地块(200, 300, 400...800),还要防止有人在你圈定的范围内凭空变出新的地块来干扰你的规划(这就是幻读问题)。
-
普通锁(记录锁)的局限: 你派人在已有的地块(200号、300号...800号)上插上旗子,宣布:“这些地块现在归项目使用,别人不能买卖或改建!”(这相当于记录锁,锁定现有行)。但这只保护了已有的地块。
-
间隙锁登场 - 锁定“虚无”: 关键来了!为了防止有人在你圈定的范围(200-800号)之内那些空着的地方或者紧挨着边界的地方突然“变”出新的地块(比如 250号、350号、450号...750号,或者在 150号、850号这种边界位置插进来),你做了更厉害的事情:
-
你在 200号地块 之前 的空地上画了一条醒目的红线,宣告:“从负无穷到200号之前的所有空地(间隙),禁止任何人在这里新建任何地块!”
-
你在 200号和300号之间 的空地上画上红线:“200号到300号之间的空地(间隙),禁止新建!”
-
你在 300号和400号之间 的空地上画上红线...
-
... 以此类推,你在 700号和800号之间 的空地上画上红线。
-
最后,你在 800号地块 之后 的空地上也画上醒目的红线,宣告:“从800号之后到正无穷的所有空地(间隙),禁止新建任何地块!”
-
这些你画在 空地 上的、禁止新建的红线,就是间隙锁!
间隙锁的核心特点:
-
锁“空”,不锁“实”: 它绝不锁定 200号、300号...800号这些已有的、具体的地块本身(那是记录锁的事)。它只锁定那些可能用来插入新地块的空隙位置。
-
锁“范围”,不锁“点”: 它不是锁一个具体的点,而是锁一段连续的、可能插入的空白区间(比如 “从负无穷到200号之前”、“200号到300号之间”、“800号之后到正无穷”)。
-
目的单一且强大:防“无中生有”: 它的唯一且最重要的目的就是:确保在你的事务(重要项目)进行期间,绝对不可能有任何新的地块(新数据行)出现在你圈定的范围(200-800号)之内或紧贴边界可能影响范围的位置。彻底消灭“幻读”(凭空冒出来的地块)。
-
代价:可能“堵车”: 副作用是,如果有人(其他事务)想在 150号空地、250号空地、750号空地、850号空地... 这些被你红线封锁的间隙里新建地块,他们会被你的红线(间隙锁)拦住,必须等待你的项目完成(事务提交),红线解除后,才能施工(插入数据)。这可能导致想新建地块的人排队等待。
总结间隙锁的精髓:
间隙锁是数据库在事务需要稳定地处理一段范围数据时,为了保护这个范围 绝对不受新插入数据干扰(防止幻读),而采取的一种特殊手段。它通过 锁定该范围内所有可能插入新数据的“空白区间”(包括范围两端的无限延伸区),像一个强大的“空间封印”,禁止任何人在这些空白地带“无中生有”地变出新数据行,直到事务结束。
间隙锁锁定的是索引记录之间的间隙,或者第一个索引记录之前,再或者最后一个索引记录之后的间隙。
如图所示位置,根据不同的查询条件都可能会加间隙锁:
例如有如下SQL,锁定的是ID (10, 20)之间的间隙,注意不包括10和20的行,目的是防止其他事务将ID值为15的列插入到列 account 表中(无论是否已经存在要插入的数据列),因为指定范围值之间的间隙被锁定了;
SELECT * FROM account WHERE id BETWEEN 10 and 20 For UPDATE;
-
间隙可以跨越单个或多个索引值;
- 当通过唯一索引精确查询单行记录时(例如
SELECT * FROM account WHERE id = 100;
,其中id
列具有唯一索引),InnoDB 仅会施加行锁(Record Lock) 在id=100
这条具体的记录上,不会使用间隙锁(Gap Lock)。 - 然而,如果
id
列未被索引,或者使用的是非唯一索引执行上述查询,InnoDB 为了保证可重复读隔离级别的语义,不仅会锁定符合条件的记录本身,还会锁定这些记录之前的间隙范围。 - 关于间隙锁的兼容性:不同事务可以在同一间隙上同时持有间隙锁。间隙锁本身没有共享(Shared)和独占(Exclusive)模式之分,它们的功能是相同的,即阻止新记录的插入。
- 最后,当事务隔离级别设置为
READ COMMITTED
时,间隙锁机制会被禁用。在该级别下,无论是普通搜索还是索引扫描,InnoDB 都不会再使用间隙锁定。
1.6 临键锁 - Next-Key Locks
什么是临键锁?
彻底理解 临键锁(Next-Key Lock)。它是间隙锁的“升级版”或“组合拳”,是数据库(尤其是 InnoDB 引擎在可重复读隔离级别下)实际最常用 来防止幻读的锁机制。
核心前提:记住间隙锁(锁空地)和记录锁(锁地块)
-
间隙锁 (Gap Lock): 锁的是地块之间以及边界之外的空白区域(可能插入新地块的位置)。比如锁
(200, 300)
这个空白区间,禁止在 200 和 300 号之间插入新地块(如 250 号)。 -
记录锁 (Record Lock): 锁的是已经存在的、具体的地块本身(如 200 号地块),防止别人买卖或改建它(修改或删除这行数据)。
临键锁(Next-Key Lock)本质上就是 间隙锁(Gap Lock) + 记录锁(Record Lock) 的组合。
我们来好好看看。
临键锁(Next-Key Lock):锁“空地 + 下一个地块”
现在,假设土地规划员(数据库事务)要执行一个操作:“找到并准备处理门牌号大于等于 200 号的下一个地块”(这对应数据库里一个非常常见的操作:SELECT ... WHERE id >= 200 FOR UPDATE
或者类似的范围查询/扫描加锁)。
-
规划员会怎么做?
-
找到目标起点: 他站在 200 号地块前。
-
锁定“空地 + 下一个地块”: 他不仅仅在 200 号地块本身 插上旗子(记录锁,锁住 200 号),更重要的是:
-
他立即在 200 号地块 之前 的那片空地 画上红线(间隙锁,锁住
(上一个地块, 200)
这个区间,比如(100, 200)
)。 -
并且,他把这两件事(锁 200 号前的空地 + 锁 200 号地块本身)视为一个不可分割的整体操作! 这个整体锁定的范围就是一个 临键锁,具体来说就是
(100, 200]
。 -
理解
(100, 200]
:-
(
:表示不包含 100 号地块(左开)。 -
100
:代表上一个实际存在的地块(比如 100 号)。 -
, 200
:表示一直到 200 号地块。 -
]
:表示包含 200 号地块本身(右闭)。
-
-
效果: 这个临键锁
(100, 200]
既禁止任何人在(100, 200)
这片空地上插入新地块(如 150 号),也禁止其他事务修改或删除 200 号这个具体的地块。
-
-
为什么叫“临键”(Next-Key)?
-
“键”(Key): 指的就是具体的门牌号(索引值),即 200 号地块本身。
-
“临”(Next): 指的是紧挨着这个键(200号)之前的那个间隙(空地),即
(100, 200)
这片区域。 -
组合起来: “临键锁” 就是 “锁定紧邻目标键值之前的间隙 + 锁定目标键值本身” 这把组合锁。它锁定了从“上一个地块之后”到“当前地块本身”的左开右闭区间。
临键锁如何工作(扫描过程)?
回到规划员的任务:“找到并处理下一个 >= 200 号的地块”。
-
锁定第一个区间: 规划员首先在 200 号上施加一个临键锁
(100, 200]
(锁空地(100,200)
+ 锁地块200
)。 -
检查地块: 他发现 200 号地块存在且符合条件(>=200)。
-
继续扫描: 规划员不会停在这里!他的任务是找到 所有 >=200 的地块。他继续往前走。
-
锁定下一个区间: 他走到了 200 号和 300 号地块之间的空地。他在这里施加下一个临键锁:锁住
(200, 300]
!这意味着:-
锁空地:锁住
(200, 300)
这片区域(禁止插入如 250 号)。 -
锁地块:锁住 300 号地块本身(防止修改/删除 300 号)。
-
-
重复直到范围结束: 规划员就这样,每遇到一个地块(键),就施加一个临键锁,锁住它之前的间隙和它本身。他会一直锁下去,直到扫描完所有满足 >=200 的地块和它们之间的间隙。对于最后一个地块(比如 800 号),他的临键锁会是
(700, 800]
,并且还会额外锁住 800 号之后直到正无穷的间隙(800, +∞)
(一个特殊的间隙锁),防止在结尾插入新的大于 800 的地块(如 900 号)。
临键锁的核心目的与威力
-
彻底消灭幻读: 这是它最主要的目的。通过锁住所有扫描路径上遇到的间隙(空地)和记录(地块):
-
防止插入: 所有可能插入新地块(新数据行)的位置(间隙)都被锁住了。没人能在 >=200 的范围内“无中生有”一个新地块(比如 250 号或 850 号)。
-
防止修改/删除导致“空缺”: 所有已经存在的、符合条件的记录(如 200、300、800 号)本身也被记录锁锁住,防止其他事务修改它们的值(比如把 200 号改成 100 号,让它不符合 >=200 条件)或者删除它们(造成一个“幻删”的错觉或留下新的可插入间隙)。这确保了事务看到的“存在的记录集合”是稳定的。
-
-
范围扫描的完美卫士: 它特别适合保护基于索引进行的范围查询(
WHERE id >= X
,WHERE id BETWEEN X AND Y
等)。扫描到哪里,锁就精确地覆盖到哪里(间隙+记录)。
总结临键锁的精髓:
临键锁是数据库为保护范围扫描(如
WHERE id >= X
)而设计的“组合锁”。它本质上是将一个间隙锁(锁定目标记录之前的空白区间)
和一个记录锁(锁定目标记录本身)
捆绑在一起,形成一个左开右闭
的区间锁(例如(Prev_Key, Current_Key]
)。当数据库扫描索引查找数据时,它会在它访问的每一个索引记录上设置一个临键锁。这个过程会:
锁住所到之处的“前一个间隙”(防插入新行)。
锁住所到之处的“当前记录”(防修改或删除该行)。
一直持续到扫描完整个所需范围,并在范围末尾锁住“正无穷间隙”。
通过这种“锁住每一寸土地(记录)和每一寸空地(间隙)”的方式,临键锁以最精确(锁到扫描点)也最强大(同时防插、防改、防删)的姿态,确保了在事务执行期间,它所查询的范围内的数据集合(存在的记录和可能出现的记录)绝对稳定不变,是解决“幻读”问题的终极武器。
Next-key 锁是索引记录锁和索引记录之前间隙上间隙锁的组合,如图所示;
当 InnoDB 搜索或扫描一个表的索引时,会采用行级锁策略。具体来说,该策略会在扫描过程中为遇到的每条索引记录设置共享锁或排他锁。因此,这种行级锁本质上就是索引记录锁 (Index Record Lock)。
此外,索引记录上的锁通常是 next-key
锁。next-key
锁不仅会锁定索引记录本身,还会锁定该记录之前的索引间隙 (Gap)。也就是说,next-key
锁 = 索引记录锁 (Record Lock) + 索引记录前的间隙锁 (Gap Lock)。
这种锁机制的效果是:如果一个会话(事务)对索引中的某条记录 R
持有共享锁或排他锁,那么其他会话(事务)就不能在索引记录 R
之前的间隙中插入新的索引记录。
假设索引包含值10、11、13和20,这些索引可能的 next-key 锁覆盖以下区间,其中圆括号表示不包含区间端点,方括号表示包含端点:
(negative infinity, 10]
(10, 11]
(11, 13]
(13, 20]
(20, positive infinity]
默认情况下,REPEATABLE READ 事务隔离级别开启 next-key 锁并进行搜索和索引扫描,可以防止幻象行(两次查询的结果集不一致),从而解决幻读问题,后面我们再分析。
1.7 插入意向锁 - Insert Intention Locks
什么是插入意向锁?
我们把插入意向锁 (Insert Intention Locks) 想象成在繁忙的超市货架上进行补货时需要遵守的“预约占位”规则。它解决的核心问题是:当货架的一段区域已经被宣布“禁止新货上架”(被间隙锁锁定)时,多个补货员(事务)如何有序地预约自己未来要放新商品的位置,避免互相踩踏,同时又不违反“禁止新货”的规定?
核心场景:货架封锁与补货需求
-
货架封锁: 想象收银员(一个事务)正在处理条码在
200
到800
之间的商品。为了防止“幽灵商品”出现(幻读),收银员已经在这些商品之间的所有空位(比如200-300
之间,300-500
之间...)以及800
之后的大片空位上拉了警戒线(间隙锁),并挂上牌子:“临时禁补区:结账中,暂停新货上架!” -
补货员到来: 这时,两个补货员(另外两个事务)推着小车来了:
-
补货员A 想把新商品
250
号奶酪 放到200
号面包和300
号鸡蛋之间的空位上(也就是间隙(200, 300)
)。 -
补货员B 想把新商品
280
号酸奶 也放到同一个间隙(200, 300)
的空位上(可能在250
旁边,也可能在同一个位置?)。
-
-
矛盾: 警戒线(间隙锁)还在呢!收银员还没结完账,牌子写着“暂停新货上架”。补货员A和B都不能立刻把奶酪和酸奶放上去,否则就违反了规则。他们必须等待收银员结完账,撤掉警戒线(间隙锁释放)。
插入意向锁登场:预约你的“坑位”
补货员A和B虽然不能立刻放货,但他们可以做一件重要的事:提前预约自己未来要放货的具体位置!
-
补货员A的行动:
-
他走到被封锁的间隙
(200, 300)
前。 -
他仔细看了看货架空位,选定了一个具体的位置,比如
250
号标签对应的空位。 -
他在这个
250
号空位前挂上一个小牌子(插入意向锁),上面写着:“预约:补货员A计划在此放入250
号奶酪(排队中)”。 -
然后他就在旁边安静等待收银员结束。
-
-
补货员B的行动:
-
他也走到同一个被封锁的间隙
(200, 300)
前。 -
他也选定了一个具体的位置,比如
280
号标签对应的空位(注意,他选的位置和A的250
是不同的)。 -
他在这个
280
号空位前也挂上一个小牌子(插入意向锁),写着:“预约:补货员B计划在此放入280
号酸奶(排队中)”。 -
他也安静等待。
-
插入意向锁的关键点:
-
只在“被封锁的空地”上挂: 只有当货架空位已经被警戒线封锁(间隙锁存在) 时,补货员才需要挂这种“预约牌子”(申请插入意向锁)。如果空地没封锁,补货员直接放货就行,不用预约。
-
预约具体位置: 插入意向锁不是锁整个大间隙(整个
(200, 300)
区域),而是精确地预约这个间隙内的一个具体的、你打算插入的键值位置(比如250
或280
)。它表明:“如果这个间隙解锁了,我就要在这个精确的点上放东西!” -
目的是“互相不打架”: 多个补货员可以在同一个被封锁的大间隙(同一个间隙锁范围内)同时挂多个预约牌(申请多个插入意向锁),只要他们预约的具体位置(键值)不同(比如
250
和280
)。这告诉超市(数据库):“我们几个都想在这个区域补货,但我们选的位置不重叠,等封锁解除后,我们可以同时、安全地放货,不会抢同一个位置打起来!” -
不冲突,可共存: 插入意向锁之间不会互相阻塞。补货员A挂
250
的牌子和补货员B挂280
的牌子可以同时存在,互不影响。它们只是在“宣告意图”。 -
等待“大锁”释放: 挂上预约牌(获得插入意向锁)并不意味着可以立刻放货!补货员A和B还是要乖乖等待收银员结完账,撤掉那个“临时禁补区”的大警戒线(间隙锁释放)。
-
释放后,按序插入: 一旦收银员结完账,撤掉警戒线(间隙锁释放),所有在这个间隙里挂了预约牌的补货员(事务)就可以同时行动了。因为他们的预约位置(
250
和280
)不同,他们可以并发地、安全地把自己的新商品(奶酪和酸奶)放到各自预约好的精确位置上,不会冲突。
如果预约位置相同会怎样?
-
如果补货员C也来了,也想在
250
号位置放另一种奶酪。他也会去挂一个预约牌:“预约:补货员C计划在此放入XXX
号奶酪(排队中)”。 -
这时,超市系统(数据库)会发现,
250
号位置已经有一个预约牌(A的插入意向锁)挂着了。 -
规则:同一个位置只能挂一个有效的预约牌! 补货员C挂牌的动作会被阻止,他必须等待补货员A完成放货动作并摘掉牌子(事务提交,释放锁)之后,才能挂上自己的牌子(申请插入意向锁成功),然后再等待间隙锁释放才能放货。这保证了同一个位置不会有两个预约。
插入意向锁的本质总结:
插入意向锁是事务在尝试向一个已经被“间隙锁”封锁的空白区域(间隙)插入新数据行时,提前在它想插入的那个精确位置(目标键值)上挂的一个“预约占位牌”。
它只做两件事:
-
宣告意图: “等这个区域解封了,我就要在这里(具体键值)插一行新数据!”
-
协调排队(仅针对相同位置): 如果多个事务都想在同一个被封锁的间隙里插入新行,只要它们想插入的具体位置(键值)不同,它们可以同时挂上“预约牌”(持有插入意向锁)一起等待。如果它们想插入的位置相同,那么后来者必须等先挂上牌的事务完成插入操作后,才能挂自己的牌(申请插入意向锁)。
插入意向锁本身并不会阻止其他事务做任何事(除了阻止别人在同一个键值上挂相同的预约牌),它最大的作用是在一个大的“禁入区”(间隙锁)内,让多个等待插入的事务能提前、友好地协调好各自未来的精确位置,一旦“禁入区”开放(间隙锁释放),它们就能立刻、并发、无冲突地在各自预约的位置上插入数据,大大提高了效率。
简言之:它是间隙锁存在时,用于在封锁区内“文明排队”、“预约坑位”,避免未来插入冲突的协作锁。
简单讲解插入意向锁
插入意向锁是一个特殊的间隙锁,在向索引记录之前的间隙进行insert操作插入数据时使用,如果多个事务向相同索引间隙中不同位置插入记录,则不需要彼此等待。
插入意向锁就是当前操作将来要获取什么样的锁以锁信息的形式保存到data_locks表里面
假设已经存在值为10和20的索引记录,两个事务分别尝试插入索引值为15和16的行,在获得插入行上的排他锁之前,每个事务都用插入意向锁锁住10到20之间的间隙,但不会相互阻塞,因为他们所操作的行并不冲突;
下面的示例演示一个事务在获得插入记录的排他锁之前,使用了插入意向锁:
会话A开启事务A | 会话B开启事务B |
#选择数据库 #插入数据 | |
# 开启事务 START TRANSACTION; | |
# 对ID大于100的索引记录设置排他锁,排他锁包括记录102之前的间隙锁 ,这里100-102之间的间隙已经加上间隙锁 SELECT * FROM child WHERE id > 100 FOR UPDATE; +-----+ | id | +-----+ | 102 | +-----+ 1 row in set (0.00 sec) | |
# 选择数据库 use test_db; # 开启事务 START TRANSACTION; | |
# 向间隙中插入一条记录, # 事务在等待获得排他锁时接受插入意图锁 INSERT INTO child (id) VALUES (101); # 开始阻塞 | |
# 查看当前状态监控中的事务信息 SHOW ENGINE INNODB STATUS\G # 如下所示 |
事务在插入阻塞的过程中,对应着一个插入意向锁,用插入意向锁来描述将来要插入的数据行相关信息。
1.8 AUTO-INC Locks
AUTO-INC锁也叫自增锁是一个表级锁,服务于配置了 AUTO_INCREMENT 自增列的表。在插入数据时会在表上加自增锁,并生成自增值,同时阻塞其他的事务操作,以保证值的唯一性。
需要注意的是,当一个事务执行新增操作已生成自增值,但是事务回滚了,申请到的主键值不会回退,这意味着在表中会出现自增值不连续的情况。
深入了解AUTO-INC锁
我们把 AUTO-INC 锁(自增锁) 也彻底讲清楚,继续用生活化的比喻,避开复杂术语和代码。
想象场景: 你正在一个非常繁忙的网红奶茶店排队。这家店有个规矩:每卖出一杯奶茶,必须给一个唯一且连续递增的号码牌(比如 101, 102, 103...)。顾客凭号码牌取奶茶,这个号码牌绝对不能重复,也不能跳号(比如不能101后面直接103)。
问题来了:
-
现在有 多个店员(代表多个事务/连接) 同时在收银台接受顾客点单。
-
每个点单都需要立刻生成一个唯一的、比上一个号码更大的新号码牌给顾客。
-
如果店员们同时生成号码牌,怎么保证不会重复?怎么保证号码是连续的?
AUTO-INC 锁 就是解决这个问题的“号码牌发放管理员”:
-
它的核心任务:确保连续递增的唯一ID
-
数据库表的
AUTO_INCREMENT
列(比如订单ID、用户ID)就像奶茶店的号码牌系统。 -
当多个事务(店员)要同时插入新记录(卖出奶茶)时,每个新记录都需要一个唯一的、比之前所有ID都大的新ID。
-
-
锁的行为(传统模式 - 最常见且容易理解):
-
想象店员A在给第一个顾客点单时,“号码牌管理员”会立刻站出来说:“等等!我现在要发下一个号码了,在我发完之前,其他人都暂停发号!”。
-
店员A拿到管理员给他的唯一新号码(比如102),写好给顾客,然后管理员才退到一边。
-
这时,店员B才能开始他的点单流程,管理员再次站出来:“停!我现在发下一个号(103)...”,如此往复。
-
关键点: 在传统模式下,每次插入新记录(点单)时,整个发号过程(获取自增ID)是串行的。即使多个事务在插入不同的行,它们获取自增ID的操作也必须一个一个排队进行。这是为了保证ID的绝对连续性和无间隙(101, 102, 103...)。
-
-
为什么需要锁?
-
防止重复: 如果没有锁,两个店员可能同时看到当前最大号是101,都决定用102,结果重复了。
-
保证连续(重要需求): 很多业务场景(如订单、流水)要求ID连续递增,不能有间隔。如果店员A拿了102,店员B拿了103,但店员A的事务因为某种原因失败了(“顾客突然不要了”),那么102这个号就废弃了,导致出现间隙(101, 103...)。AUTO-INC锁的传统模式在获取ID时就锁住,很大程度上避免了这种因事务回滚导致的间隙(但不是100%,比如批量插入时中间号可能浪费)。
-
简单粗暴有效: 串行化发号是最简单、最可靠保证唯一且连续递增的方式。
-
-
轻量级模式(改进版):
-
传统模式虽然可靠,但效率不高(每次插入都要排队等发号)。
-
新版本的MySQL(InnoDB)引入了一种 “轻量级自增锁” 模式。
-
比喻: 想象管理员这次不是每次都站出来让所有人暂停,而是提前划好一段号码范围(比如102-105)给某个店员。
-
店员A点单时,管理员悄悄告诉他:“这4个号(102-105)归你了,你给接下来的4个顾客用吧,用完再找我领新的”。店员A就可以连续给4个顾客发102, 103, 104, 105,中间不需要每次点单都找管理员排队。
-
同时,店员B来点单,管理员也划一段不同的连续号码给他(比如106-110)。店员A和店员B就可以同时给各自的顾客发号了!
-
关键点:
-
轻量级锁在语句执行开始时就知道要插入多少条记录(比如
INSERT INTO ... VALUES (...), (...), (...)
插入3条),它会一次性申请一个足够大的连续ID范围(比如3个号)。 -
只要多个插入语句需要的ID范围不重叠,它们就可以并发获取自增ID,大大提高了并发性能。
-
但单个语句内部的多条记录,其ID仍然是连续的(店员A发的102,103,104是连续的)。
-
不同语句生成的ID范围之间,可能不再是紧密连续的(店员A发完105,店员B接着发106,但如果店员A只用了102和103,那么104和105就浪费了,下个号是106)。所以轻量级锁下,全局ID可能不再保证绝对连续无间隙,但仍然保证唯一且整体递增。这对于大多数不需要绝对连续ID的场景是性能和可靠性的很好平衡。
-
-
1.9 死锁
示例
由于每个事务都持有另一个事务所需的锁,导致事务无法继续进行的情况称为死锁。以下图为例,两个事务都不会主动释放自己持有的锁,并且都在等待对方持有的资源变得可用。
下面通过一个示例演示一下死锁的发生过程,其中涉及两个客户端A和B,并通过启用全局变量
innodb_print_all_deadlocks 来查看死锁的信息,同时死锁信息也会保存到错误日志中
首先打开一个客户端A,并执行以下操作
首先先选择一个数据库db1
客户端A启用innodb_print_all_deadlocks
创建两个张表animals和birds
分别向两张表中写入数据
客户端A开启一个事务
使用共享锁查询animals表中的某一行
当前事务A对animals表里面的一条记录已经加了共享锁(s)。
接下来,打开客户端B并执行以下操作
选择该数据库,然后开启一个事务B
使用共享锁查询birds表中的某一行
当前事务A对animals表里面的一条记录已经加了共享锁(s)。
在第3个客户端中查看两个select操作持有的锁信息
目前来说是下面这个图这种情况
在客户端B中更新animals表中的行
更新animals表中的行
客户B开始等待。
在客户端C可以查看锁的等待信息:
查看等待中的锁
查看锁信息
现在的情况就是还没有形成死锁,只要事务A回滚,那么就不会形成死锁
如果客户端A试图同时更新birds中的一行,将导致死锁
更新birds中的某一行,此是发生死锁
死锁发生时,InnoDB主动回滚导致死锁的事务,此时可以看到客户端B的更新执行成功。
之前的事务B的更新操作也终于执行成功了。
中间等待了很久,直到客户端A触发死锁
- 可以通过以下方式查看当前服务器发生死锁的次数
- 查看当前服务器发生死锁的次数
-
InnoDB的监视器包含了关于死锁和事务的相关信息,可以通过 SHOW ENGINE INNODB STATUS;查看 LATEST DETECTED DEADLOCK 节点的内容
错误日志中也记录了死锁相关的信息
查看错误日志路径
SELECT @@log_error;
tail -n 100 /var/log/mysql/error_log.err
1.9.1.死锁产生的条件(非常重要)
这个非常重要,面试的时候经常问!!!
死锁产生的必要条件(必须同时满足):
-
互斥访问 (Mutual Exclusion): 锁具有独占性。若一个线程已持有某把锁(如锁A),则其他线程在该线程释放前无法同时持有这把锁。
-
不可抢占 (No Preemption): 锁不能被强制剥夺。一个线程已获得的锁,只能由该线程主动释放,不能被其他线程强行抢占。
-
持有并等待 (Hold and Wait): 线程在持有至少一把锁(如锁A)的同时,还在尝试获取其他线程持有的另一把锁(如锁B)。
-
循环等待 (Circular Wait): 线程1等待线程2释放锁,线程2也等待线程1释放锁,死锁发生时系统中一定有由两个或两个以上的线程组成的一条环路,该环路中的每个线程都在等待着下一个进程释放锁。(例如:线程 T1 持有锁A并等待T2持有的锁B,而线程 T2 持有锁B并等待T1持有的锁A)。
打破死锁:
由于死锁的发生要求上述四个条件同时成立,因此,预防或解除死锁的策略就是破坏其中至少一个条件。最常见的策略是破坏“循环等待”条件,具体实现方式包括:
-
锁排序 (Lock Ordering): 为系统中所有可能用到的锁类型定义一个全局的、严格的申请顺序。要求所有线程必须严格按照这个顺序申请所需的所有锁。这从根本上消除了线程之间形成“你等我锁,我等它锁,它又等你锁”的循环等待链的可能性。
-
一次性申请所有锁 (Lock Acquisition All at Once / Coarse-Grained Locking): 要求线程在执行临界区代码前,一次性申请其所需的所有锁。如果无法同时获得所有锁,则线程不持有任何锁并等待或重试。这破坏了“持有并等待”条件(通常也间接避免了循环等待)。虽然简单有效,但可能降低并发度。
1.9.2.InnoDB对死锁的检测
InnoDB 在运行时具备死锁检测能力。当死锁检测功能启用时(默认开启),InnoDB 会自动监控事务间的依赖关系,一旦检测到死锁,便会主动回滚一个或多个事务以解除死锁状态(通常是谁触发死锁,谁回滚)。
-
回滚策略: InnoDB 通常倾向于回滚较小的事务。事务大小的衡量标准是其已执行的插入、更新或删除操作的行数。
-
检测范围:
-
在系统变量
innodb_table_locks = 1
(默认值) 且autocommit = 0
的情况下,InnoDB 能够检测到同时涉及表级锁(例如通过LOCK TABLES
语句设置)和其自身管理的行级锁的死锁。 -
否则,对于纯粹由
LOCK TABLES
设置的表级锁或由非 InnoDB 存储引擎设置的锁引发的死锁,InnoDB 可能无法检测到。
-
-
超时处理: 对于无法自动检测的死锁场景,可以通过设置系统变量
innodb_lock_wait_timeout
的值来指定锁等待超时时间。当事务等待锁的时间超过此阈值时,将被强制回滚,从而解决死锁问题。 -
高负载处理: 当系统出现超过 200 个事务同时等待锁资源,或者等待队列中的锁请求总数超过 1,000,000 个时,InnoDB 会将此视为潜在的严重资源争用(类似于死锁风险),并尝试回滚等待列表中的部分事务以缓解压力。
-
性能考量与禁用选项: 在高并发系统中,如果大量线程竞争相同的锁,死锁检测本身可能成为性能瓶颈。此时,禁用死锁检测并完全依赖
innodb_lock_wait_timeout
设置进行超时回滚,反而可能获得更高的整体吞吐量。可以通过设置系统变量innodb_deadlock_detect
(可选值OFF
或ON
)来禁用死锁检测功能。
1.9.3.如何避免死锁
MySQL是一个多线程程序,死锁的情况大概率会发生,但他并不可怕,除非频繁出现,导致无法运行某些事务。
InnoDB使用自动行级锁,即使在只插入或删除单行的事务中,也可能出现死锁(因为涉及到索引的修改)。这是因为插入或删除行并不是真正的“原子”操作,同时会对索引记录进行修改并设置锁
降低死锁发生概率和处理死锁的实用技术:
-
优先使用事务,避免手动表锁: 使用事务代替
LOCK TABLES
语句进行手动加锁。同时,配置innodb_lock_wait_timeout
参数,设置合理的锁等待超时时间,确保在任何情况下锁都能自动释放,防止长时间阻塞。 -
诊断死锁原因:
-
定期执行
SHOW ENGINE INNODB STATUS
命令,检查输出中的LATEST DETECTED DEADLOCK
部分,分析最近一次死锁的详细信息。这有助于定位问题并修改应用程序逻辑以避免死锁。 -
若频繁遭遇死锁警告,可临时启用
innodb_print_all_deadlocks
系统变量。启用后,所有死锁的详细信息将被记录到 MySQL 错误日志中,便于全面调试。调试完成后务必禁用此选项。
-
-
处理死锁失败: 当事务因死锁而失败(回滚)时,应用程序应具备重试机制,应该重新发起并执行该事务。
-
优化事务设计:
-
避免大事务: 尽量保持事务短小精悍(Small and Short-lived)。较小、耗时短的事务发生冲突的概率更低,能有效减少死锁。
-
及时提交: 完成修改后立即提交事务。特别注意:避免在交互式会话(如命令行客户端)中长时间保持一个未提交的事务打开。
-
保持一致的访问顺序: 当事务需要修改多个表或同一表中的多行时,所有事务都应遵循完全相同的操作顺序(如表 A 总是先于表 B,或行 ID 按固定顺序处理)。这确保了操作队列的一致性,是避免循环等待死锁的关键策略。将相关操作封装在统一的方法或函数中有利于维持顺序一致。
-
-
优化查询与索引:
-
添加合适的索引: 确保表有恰当的索引,使查询能高效定位数据,减少需要扫描的行数和锁的数量。使用
EXPLAIN SELECT
分析查询执行计划,确认是否使用了最优索引。
-
-
考虑锁粒度与隔离级别:
-
使用表级锁 (谨慎使用): 在特定场景下,使用
LOCK TABLES
进行表级锁可以阻止并发更新,从而避免死锁。但代价是显著降低系统的并发性能。 -
降低隔离级别: 如果查询使用了显式锁(如
SELECT ... FOR UPDATE
或SELECT ... FOR SHARE
),考虑将事务隔离级别降至READ COMMITTED
。该级别减少间隙锁(Gap Locks)的使用,有助于降低某些类型死锁的发生概率。
-