mysql锁
一:如果 MySQL 不带索引执行 DML,会锁表吗?
问题:当 MySQL 的 UPDATE( 或 DELETE) 没有加索引的字段时,到底会不会锁整张表?在实际开发中经常遇到:“ 写了个
UPDATE user SET age = 25 WHERE name = '张三',但发现,别的同事连查都不行了,是不是把表锁死了?”在高并发系统里, 还 可能直接导致服务卡顿甚至崩溃。这是 为什么?或者说: 如果 MySQL 基于不带索引的列执行 DML,会锁表吗?先说结论:
❌ 它造成的后果,跟锁表几乎一模一样——别人动不了数据,只能干等。本质上,是加了三个锁: “行锁 + 间隙锁 + 意向锁” 。尼恩总结: 对 非索引列执行 DML ,InnoDB 会触发一系列连锁锁操作,最终表现为 “锁表” 效果。其本质是 “行锁 + 间隙锁 + 意向锁” 的组合。或者说: “行锁 + 间隙锁 + 意向锁” = “伪锁表”, 而不是真的锁表。
1.1、InnoDB 锁机制的核心前提:依赖索引
InnoDB 的行级锁(Record Lock)设计初衷是实现精准的并发控制,其核心特性是通过索引定位目标行,仅对匹配的行加锁。这意味着:- 当 DML 语句(UPDATE/DELETE)的
WHERE条件使用索引列时,InnoDB 能直接通过索引找到目标行,仅锁定这些行,其他行不受影响。 - 当
WHERE条件使用非索引列时,InnoDB 无法通过索引定位行,只能通过全表扫描遍历所有记录,锁行为会因此发生根本性变化。
UPDATE users SET nickname = 'VIP用户' WHERE real_name = '李雷';
这条语句看起来没问题,对吧?可问题是,real_name 这个字段没建索引!结果呢?其他同事想改某个用户的手机号(完全无关的人)、新增用户、甚至管理员想删个测试账号……全都卡住了!数据库像死了一样。这时候大家就开始怀疑:“是不是把表锁住了?”于是问题来了:MySQL 到底是怎么处理这种情况的?它真的会锁表吗?要回答这个问题,得先了解 MySQL 用的存储引擎——InnoDB 是怎么加锁的。
1.2、InnoDB 加锁的前提:靠索引找数据
MySQL 默认使用的 InnoDB 引擎,支持的是“行级锁”,也就是说,理想情况下,它只锁定你要改的那一行,不影响其他人操作别的行。但这有一个关键前提:必须能快速定位到目标行。怎么定位?靠索引。举个例子你就明白了:- 假设你要从一万本书里找一本《三体》,如果你有目录(相当于索引),几秒就能翻到;
- 如果没有目录,你就只能一本一本地翻,效率极低。
| 条件列是否有索引 | 查找方式 | 锁的影响范围 |
| 有索引 | 直接跳过去 | 只锁匹配的那几行 |
| 没索引 | 全表扫描 | 扫过的每一行都可能被锁 |
只要你的 WHERE 条件用的是没有索引的字段,InnoDB 就没法精准定位,只能一行一行地扫过去看符不符合条件。而在这个过程中,为了保证数据安全,它会对所有“看过的”记录都尝试加锁。这就埋下了“伪锁表”的隐患。
1.3、真实案例演示:一次 UPDATE 如何“间接锁表”
我们来看一个具体的例子。1. 准备一张简单的用户表
CREATE TABLE user (
id INT PRIMARY KEY, -- 主键,自带索引
name VARCHAR(100) -- 普通字段,没加索引
) ENGINE=InnoDB;
INSERT INTO user VALUES (1, '张三'), (2, '李四'), (3, '王五');
这张表很简单,
id是主键,有索引;name字段没有任何索引。
-- 事务 T1
BEGIN;
UPDATE user SET name = '张三_new' WHERE name = '张三';
注意:这里的 WHERE name = '张三' 是基于非索引列查询的。这时候会发生什么?
2. InnoDB 加了 三种锁 “行锁 + 间隙锁 + 意向锁” 的组合 ?
想改“张三”这一条记录,但因为name 没索引,InnoDB 必须这么做:
“ 把这张表从头到尾每一条都看一下,看看谁叫‘张三’。”于是它开始全表扫描,在这个过程中,它会逐步加上几种不同的锁:
(1)意向排他锁(IX 锁)——表级锁
意向锁,是 “预告”, 这里是 表级别的“预告”这是一种轻量级的表级锁,表示:“我要开始修改这张表里的某些行了,请别在这时候给我整个表上大锁。”作用是防止别人执行LOCK TABLES ... WRITE 这种粗暴的操作,但它本身不阻止其他事务加行锁。注意,IX 锁 不阻止其他事务加行锁,IX 锁 不阻止其他事务加行锁, IX 锁 不阻止其他事务加行锁。
(2)行锁(X 锁)——行级锁
当 InnoDB 扫到id=1 这一行,发现 name='张三',符合条件,这时候出大招了。InnoDB 会给这一行加上排他锁(X Lock),意味着别人不能读也不能改这一行(直到你提交事务)。
(3)间隙锁(Gap Lock)——防“插队”
InnoDB 不仅要保护已有的数据,还要防止别人趁机插入新的记录,造成“幻读”。比如, 在 扫描的过程,如果其他人 插入了一个id=4, name='张三' 的新用户,那你这次更新就没覆盖到,数据就不一致了。为了避免这种情况,InnoDB 会给各个“空隙”加锁。为防止 “幻读”(其他事务插入新行导致查询结果变化),InnoDB 会对全表扫描过程中涉及的所有 “间隙” 加锁。对于本例,间隙包括:
(-∞, 1):小于id=1的区间(如阻止插入id=0);(1, 2):id=1和id=2之间的区间;(2, 3):id=2和id=3之间的区间;(3, +∞):大于id=3的区间(如阻止插入id=4)。
(4)临键锁(Next-Key Lock)——行锁 + 间隙锁的组合
间隙锁(Gap Lock)和 行锁(X 锁)是理论层的锁,临键锁(Next-Key Lock) 是 实际层的锁。 (稍后尼恩给大家详细介绍)。InnoDB 实际使用的是“临键锁”,它是行锁和间隙锁的结合体。例如:- 对
id=1的行加锁的同时,也锁住(1,2)这个区间; - 同理,扫描到
id=2时也会检查并锁定对应区间。
1.4、为什么说 “行锁 + 间隙锁 + 意向锁” 效果等于锁表?
现在我们来看看, 另一个事务会发生什么。 假设,这时另一个同事想做个无关的修改:
-- 事务 T2(会被阻塞)
BEGIN;
UPDATE user SET name = '李四_new' WHERE id = 2;
按理说,T1 改的是 name='张三',T2 改的是 id=2 的人,互不相干,应该可以并行才对。但事实是:T2 会被阻塞,一直等到 T1 提交或回滚!为什么会这样?因为你忘了:T1 正在全表扫描,它已经对 id=2 这一行进行了判断(虽然不符合条件),并且在这个过程中加了间隙锁或临键锁,导致 T2 想改这一行就必须等待锁释放。同样的道理:
-- 插入新用户?不行!
INSERT INTO user VALUES (0, '赵六'); -- 被间隙锁挡住
-- 删除王五?也不行!
DELETE FROM user WHERE id = 3; -- 扫描过程已锁定该行
你会发现,不管你想做什么写操作——改、删、增,统统被挡在外面。虽然技术上 InnoDB 并没有执行 LOCK TABLES 这样的命令,也没有加一个真正的“表级写锁”,但由于全表扫描 + 行锁 + 间隙锁的连锁反应,最终结果就是:
整张表的写操作都被冻结了,就像被锁住了一样。这就是所谓的“伪锁表现象”。
1.5、总结:不是锁表,胜似锁表
回到最初的问题:如果 MySQL 基于不带索引的列执行 DML,会锁表吗?答案是:
❌ 不会直接加表级锁, 强调一下, 不会主动加表级锁(如 LOCK TABLES)。✅ 但由于全表扫描触发了大量的 “行锁 + 间隙锁 + 意向锁” ,导致其他所有写操作都无法进行,实际效果和锁表几乎没有区别。这就像你在高速公路上修一条车道,理论上只影响一条道,但如果施工车横着停,把所有车道都占了,那结果就是全线瘫痪。基于不带索引的列执行 DML 时:
- InnoDB 不会直接加表级锁,而是通过 “行锁 + 间隙锁 + 意向锁” 的组合实现并发控制;
- 由于全表扫描的必然性,锁范围会扩大到整个表,导致其他事务无法操作任何行,效果等同于锁表。
最佳实践: 给常用查询条件字段加索引!
尤其是那些经常出现在WHERE 子句中的字段,比如:
statuscreated_timeuser_idemailname(如果常用来查)
ALTER TABLE user ADD INDEX idx_name (name);
加完索引后再执行同样的更新:
UPDATE user SET name = '张三_new' WHERE name = '张三';
这时 MySQL 会通过 idx_name 索引迅速定位到对应行,只锁这一行及其附近的小范围间隙,其他事务完全不受影响。
定期审查慢查询日志
如果你发现某些 UPDATE 或 DELETE 特别慢,或者频繁引起锁等待,可以用SHOW ENGINE INNODB STATUS\G 查看锁信息,或者开启慢查询日志,找出哪些语句在做全表扫描。
开发规范中明确要求:禁止对无索引字段做 DML 条件
可以在团队内部制定规则,类似:“所有用于 WHERE 条件的非主键字段,必须评估是否需要建立索引。”
“上线前需通过 SQL 审核工具检查是否存在无索引条件更新。”
二:三大锁 临键锁/ 间隙锁 / 行锁 , mysql源码中 对应 几个锁对象?
“行锁”、“间隙锁”、“临键锁”,听起来挺玄乎的。当我们在一个没有索引的字段上做 UPDATE 的时候,MySQL 源码 到底加了多少个锁?这些锁到底是怎么组织的?
2.1 先看个例子:一条简单的更新语句
假设我们有张表:
CREATE TABLE user (
id INT PRIMARY KEY, -- 主键索引
name VARCHAR(100) -- 无索引
) ENGINE=InnoDB;
INSERT INTO user VALUES (1, '张三'), (2, '李四'), (3, '王五');
现在有个事务要改名字:
-- 事务 T1
BEGIN;
UPDATE user SET name = '张三_new' WHERE name = '张三';
注意!这里 WHERE name = '张三' 是在 没有索引的列 上做的条件判断。那问题来了:
这条语句执行时,MySQL 内部到底创建了几个“锁对象”?是分开的行锁和间隙锁吗?还是别的形式?很多人会想:“哦,既然是行锁 + 间隙锁,那就是两个锁。”但其实——不是这样的。 真实情况比这复杂。
2.2 背景分析:为什么这个操作这么“重”?
先搞明白一件事:因为name 没有索引,所以 MySQL 根本没法快速定位到 ‘张三’ 这条记录。它只能怎么办?—— 全表扫描!也就是说,InnoDB 会从主键索引的第一个记录开始,一条一条地读,检查每条记录的 name 是否等于 '张三'。这种操作叫 基于主键索引的全扫描。而只要涉及扫描,在 REPEATABLE READ 隔离级别下(MySQL 默认),InnoDB 就必须防止“幻读”——也就是别人不能在这期间插入新的满足条件的记录。怎么防?靠的就是 临键锁(Next-Key Lock)。
2.3 什么是临键锁?别被名字吓住
你可以把“临键锁”想象成一把“带地盘的锁”。它锁的不只是某一行数据,还包括:- 这一行本身(行锁)- 和它前面的一段“空隙”(间隙锁)。注意,是前面的一段“空隙”、前面的一段“空隙”、前面的一段“空隙”(说3编)比如,对于主键id=2,它的临键锁可能是 (1, 2] —— 表示从 1 到 2 这个区间都不能插入新数据,同时 id=2 这行也不能被修改。所以,临键锁 = 行锁 + 前一个间隙锁,是一个整体,不是两个独立的锁。关键点:在 MySQL 源码中,并没有单独的“行锁对象”或“间隙锁对象”,只有“临键锁对象”,通过标记位来区分它是纯行锁、纯间隙锁,还是两者都有。
2.4 InnoDB 是怎么实现这些锁的?(源码视角 )
InnoDB 用一个叫lock_t 的结构体来表示每一个锁。每个锁对象可以是:
- 表级锁(比如意向锁)
- 记录级锁(作用于某条索引记录)
UPDATE 语句时,会发生下面这些事:
第一步:先加一个表级“意向排他锁”(IX 锁)
这是为了告诉其他事务:“我要动这张表里的某些行了,请别想着加表级共享锁了。”数量:1 个 IX 锁对象这个锁是全局唯一的,属于整张表。第二步: 然后对每一行扫描到的记录加临键锁
由于是全表扫描,InnoDB 必须为每一个可能影响的“索引位置”都加上锁,防止别人插队。我们的数据是:- id=1- id=2- id=3主键索引有序排列,InnoDB 会在以下四个范围上各加一个临键锁:| 锁范围 | 对应记录 | 说明 |
(-∞, 1] |
id=1 | 锁住最小值之前的空隙和第一行 |
(1, 2] |
id=2 | 锁住 1~2 之间的空隙和第二行 |
(2, 3] |
id=3 | 锁住 2~3 之间的空隙和第三行 |
(3, +∞) |
—— | 特殊!这是最后一个间隙,没有对应行 |
(3, +∞) 是个“间隙锁”,但它仍然是以“临键锁”的形式存在的,只不过它只锁间隙,不锁行(因为后面没数据了)。在源码中,这种锁会被标记为 LOCK_GAP 属性,并指向一个“虚拟记录”(ghost record),用来代表无穷大。所以这里一共产生了 4 个临键锁对象
尼恩提示:虽然我们只改了一行(id=1),但由于没有索引,MySQL 不知道哪一行符合条件,只能把所有可能的地方都锁住,以防万一。
2.4 问题来了:“行锁/ 间隙锁/临键锁 ” 如何区分?
很多文章喜欢说:“临键锁就是行锁 + 间隙锁”。这话没错,但从 实现角度来说容易误导人。真相是:在 MySQL 源码中,不存在独立的“行锁对象”和“间隙锁对象”。所谓的“行锁”和“间隙锁”,只是临键锁的两种 “理论类型”/ 功能描述.“行锁/ 间隙锁/临键锁 ” 靠
lock_mode 标志位 区分。lock_mode 标志位:
| 标志位 | 含义 |
LOCK_REC_NOT_GAP |
只锁记录,不锁前后间隙(就是“行锁”) |
LOCK_GAP |
只锁间隙,不锁记录(就是“间隙锁”) |
| (默认无特殊标志) | 同时锁记录和前一个间隙(就是“临键锁”) |
| 锁范围 | 对应记录 | 说明 |
(-∞, 1] |
id=1 | 锁住最小值之前的空隙和第一行 |
(1, 2] |
id=2 | 锁住 1~2 之间的空隙和第二行 |
(2, 3] |
id=3 | 锁住 2~3 之间的空隙和第三行 |
(3, +∞) |
—— | 特殊!这是最后一个间隙,没有对应行 |
- 1 个 IX 锁对象(表级意向排他锁)
- 4 个临键锁对象(每个对应一个索引项及其前向间隙)
(1,2])- 第 4 个 (3,+∞) 是纯间隙锁(LOCK_GAP 模式),用于阻止插入新记录它们都是通过 lock_rec 结构体创建的单个锁对象,并不是把行锁和间隙锁拆成两个对象分别存储。
2.6、为何不拆分为独立的行锁和间隙锁对象?
InnoDB 源码设计中,行锁和间隙锁并非独立的锁对象,而是临键锁的属性:- 当临键锁的范围仅包含行记录(无间隙)时,表现为 “行锁”(
LOCK_REC_NOT_GAP属性)。 - 当临键锁的范围仅包含间隙(无行记录)时,表现为 “间隙锁”(
LOCK_GAP属性)。 - 默认情况下,临键锁同时包含行记录和间隙(无特殊属性),即 “行锁 + 间隙锁” 的组合。
LOCK_GAP、LOCK_REC_NOT_GAP等)区分其行为,而非创建多个锁对象。
2.7、总结
在 MySQL 源码中,案例场景的锁对象数量为:- 1 个 IX 锁对象(表级);
- 4 个临键锁对象(行级,包含行锁和间隙锁的功能)。
这种设计既保证了锁的粒度(行级),又通过临键锁的范围覆盖实现了防幻读的目标,同时减少了锁对象的数量,提升了并发控制的效率。
https://mp.weixin.qq.com/s/5NtmdkXZ2sFCdHufo348jQ?scene=1&click_id=175
个人学习笔记,记录日常学习,便于查阅及加深,仅为方便个人使用。

浙公网安备 33010602011771号