mysql中的普通的update,delete,INSERT语句是否会加锁
MySQL 中普通的 UPDATE、DELETE、INSERT 语句都会加锁,且核心锁行为依然由「存储引擎」和「索引」决定 —— 写操作(增删改)需要保证数据一致性,避免并发冲突(如脏写、丢失更新),因此默认会加排他锁(X 锁) (最严格的锁,禁止其他事务读锁 / 写锁竞争)。
以下是分存储引擎、分语句的详细拆解,重点聚焦实际开发常用的 InnoDB 引擎:
一、核心结论先明确
| 存储引擎 | 语句类型 | 是否加锁 | 锁类型(默认) | 关键依赖 |
|---|---|---|---|---|
| InnoDB(主流) | UPDATE/DELETE | 是 | 行级排他锁(X 锁) | 依赖索引(无索引 / 失效 → 表锁) |
| InnoDB | INSERT | 是 | 隐式排他锁 + 间隙锁 | 主键 / 唯一索引防冲突,RR 级别防幻读 |
| MyISAM(淘汰) | UPDATE/DELETE/INSERT | 是 | 表级排他锁(X 锁) | 无(仅表锁,不支持行锁) |
核心逻辑:写操作必须加排他锁(X 锁),锁粒度由 “能否通过索引定位行” 决定(InnoDB 场景) ,MyISAM 因无行锁支持,直接锁全表。
二、分场景详细解释(重点 InnoDB)
1. InnoDB 存储引擎(事务型,支持行锁)
InnoDB 中,增删改语句的锁行为围绕「排他锁(X 锁)」展开,目的是阻止其他事务同时修改同一数据,保证事务隔离性。
(1)UPDATE & DELETE:行级排他锁(依赖索引)
这两个语句逻辑高度一致:先 “找到要修改 / 删除的行”,再给这些行加排他锁,最后执行操作。
-
有索引时(主键 / 二级索引) :加「行级排他锁(X 锁)」
-
原理:通过索引精准定位到目标行对应的「索引项」,给索引项加 X 锁(InnoDB 行锁本质是锁索引项),进而锁定对应数据行;
-
效果:仅锁定满足
WHERE条件的行,其他行不受影响,并发性能高; -
举例:
-- user 表 id 是主键索引(有索引) UPDATE user SET age=20 WHERE id=1; -- 仅给 id=1 的行加行级 X 锁 DELETE FROM user WHERE name='张三'; -- name 是二级索引,仅给 name='张三' 的行加 X 锁 -- 其他事务修改 id=2、name='李四' 的行不受阻塞
-
-
无索引 / 索引失效时:升级为「表级排他锁(X 锁)」
-
原理:无索引时无法精准定位行,只能全表扫描逐行判断是否符合条件;此时逐行加锁会导致开销极大 + 隔离性破坏(如幻读),InnoDB 直接升级为表锁;
-
效果:整个表被加 X 锁,其他事务的所有写操作(INSERT/UPDATE/DELETE)和加锁读(
FOR UPDATE)都会被阻塞,并发性能极差; -
举例:
-- age 无索引(或用了函数操作导致索引失效:WHERE DATE(create_time)='2025-01-01') UPDATE user SET name='test' WHERE age=30; -- 升级为表级 X 锁 -- 其他事务执行 INSERT INTO user(name) VALUES('李四') 会被阻塞,直到当前事务提交
-
(2)INSERT:隐式排他锁 + 间隙锁(特殊逻辑)
INSERT 是新增数据,不存在 “锁定已有行” 的问题,但为了避免并发冲突(如主键重复)和幻读,会加两种特殊锁:
-
① 「隐式排他锁」:针对新增的行本身
- 原理:插入时,InnoDB 会给新生成的行自动加隐式 X 锁(无需显式声明),阻止其他事务同时修改或删除这行;
- 释放时机:事务提交 / 回滚后自动释放;
- 冲突场景:若两个事务同时插入相同主键的行(如
INSERT INTO user(id) VALUES(1)),第一个事务成功加锁插入,第二个事务会因主键冲突阻塞,直到第一个事务提交(第二个事务报错 “主键重复”)或回滚(第二个事务成功插入)。
-
② 「间隙锁 / Next-Key Lock」:针对插入位置的间隙(仅 RR 隔离级别,默认)
-
原理:为了避免 “幻读”(比如事务 A 插入
id=5的行,事务 B 同时插入id=5的行),InnoDB 会锁定新增行相邻的 “间隙”(比如主键索引中id=4和id=6之间的间隙); -
效果:阻止其他事务在该间隙插入相同键值的行,保证 RR 隔离级别的 “可重复读”;
-
例外:RC(读已提交)隔离级别下,间隙锁会失效(仅保留记录锁),此时可能出现幻读,但并发性能略高;
-
举例:
-- 事务 A(RR 隔离级别,id 是主键) INSERT INTO user(id, name) VALUES(5, '王五'); -- 锁定 id=4~6 之间的间隙 -- 事务 B 执行 INSERT INTO user(id, name) VALUES(5, '赵六'); 会阻塞(主键冲突+间隙锁) -- 事务 C 执行 INSERT INTO user(id, name) VALUES(4.5, '孙七'); 若 id 是整数,间隙锁也会阻止(实际是锁定 4~6 区间) COMMIT; -- 事务 A 提交后,B 报错,C 若 id 合法则成功
-
2. MyISAM 存储引擎(淘汰,仅作了解)
MyISAM 不支持事务和行锁,只有「表级锁」,所有写操作(UPDATE/DELETE/INSERT)都会加「表级排他锁(X 锁)」:
-
效果:加 X 锁后,整个表被锁定,其他事务的所有操作(读:SELECT 加 S 锁;写:INSERT/UPDATE/DELETE 加 X 锁)都会被阻塞;
-
问题:并发性能极差(写阻塞读,读阻塞写),这是 MyISAM 被 InnoDB 淘汰的核心原因;
-
举例:
-- MyISAM 表 UPDATE user SET age=20 WHERE id=1; -- 加表级 X 锁 -- 其他事务执行 SELECT * FROM user; 会被阻塞,直到当前 UPDATE 执行完毕
三、关键注意事项(实际开发必看)
-
锁的释放时机:InnoDB 中,写操作的锁(行锁 / 表锁 / 间隙锁)都是「事务级锁」,只有事务提交(COMMIT)或回滚(ROLLBACK)后才会释放;若事务长时间不提交,会导致锁等待超时(默认超时时间由
innodb_lock_wait_timeout控制,默认 50 秒)。 -
索引是行锁的前提:和 UPDATE/DELETE 一样,INSERT 的间隙锁也依赖索引(尤其是主键 / 唯一索引);若插入的字段无索引,可能导致间隙锁范围扩大,影响并发。
-
隔离级别的影响:
- RR(默认):INSERT 会加间隙锁 / Next-Key Lock,防幻读,但锁粒度略粗;
- RC:INSERT 仅加隐式行锁,间隙锁失效,可能出现幻读,但并发更高;
-
避免长事务:写操作若在长事务中执行,锁会长期持有,容易引发锁等待、死锁(比如两个事务互相持有对方需要的锁),实际开发中要尽量缩短事务周期。
四、select语句查询是否会有锁
MySQL 中普通 SELECT 语句是否加锁,核心取决于存储引擎和事务隔离级别,其中 InnoDB(默认事务型引擎)和 MyISAM(传统非事务型引擎)的行为差异极大,以下是详细拆解:
一、核心结论先明确
| 存储引擎 | 事务隔离级别(InnoDB) | 普通 SELECT 是否加锁 | 关键说明 |
|---|---|---|---|
| InnoDB(主流) | RC(读已提交)/ RR(可重复读,默认)/ RU(读未提交) | 不加锁(快照读) | 基于 MVCC 实现 “非锁定读”,不阻塞读写 |
| InnoDB | SERIALIZABLE(串行化) | 加锁(共享锁) | 强制当前读,阻塞写操作 |
| MyISAM(淘汰) | 无(不支持事务) | 加锁(表级共享锁) | 读锁阻塞写,写锁阻塞读 |
二、分场景详细解释
1. InnoDB 存储引擎(重点,实际开发 99% 用它)
InnoDB 支持事务和行锁,普通 SELECT 的锁行为由 隔离级别 和 是否显式加锁 决定,核心依赖 MVCC(多版本并发控制) 机制。
(1)默认场景:RC/RR 隔离级别(普通 SELECT 不加锁)
这是最常用的场景,普通 SELECT 属于 快照读(一致性非锁定读) :
-
原理:InnoDB 会通过
undo 日志读取数据的 “历史版本”(而非最新版本),不需要加锁; -
效果:
-
读操作不会阻塞写操作(即使读同一行,写操作也能正常执行,只是写的是最新版本);
-
写操作也不会阻塞读操作(读的是历史快照,不依赖最新数据);
-
举例:
-- 事务A(RR隔离级别) START TRANSACTION; SELECT * FROM user WHERE id=1; -- 快照读,不加锁,读取id=1的历史版本 -- 此时事务B执行 UPDATE user SET name='test' WHERE id=1; 可以正常执行,不会被阻塞 COMMIT;
-
-
例外:如果查询条件无法命中索引(全表扫描),InnoDB 可能会退化为准表锁(间隙锁 / Next-Key Lock),但这是 “锁范围扩大”,而非普通
SELECT主动加锁。
(2)特殊场景 1:SERIALIZABLE 隔离级别(加共享锁)
SERIALIZABLE 是最高隔离级别,为了避免 “幻读”,普通 SELECT 会被强制转为 当前读(锁定读) ,并加 表级 / 行级共享锁(S 锁) :
-
效果:
- 加 S 锁后,其他事务可以读,但无法写(写需要加排他锁 X,会被 S 锁阻塞);
- 其他事务的写操作会阻塞,直到当前事务提交 / 回滚释放 S 锁;
-
举例:
-- 事务A(SERIALIZABLE隔离级别) START TRANSACTION; SELECT * FROM user WHERE id=1; -- 加行级S锁 -- 此时事务B执行 UPDATE user SET name='test' WHERE id=1; 会阻塞,直到事务A提交 COMMIT; -- 释放S锁,事务B才会执行
(3)特殊场景 2:显式加锁的 SELECT(主动加锁)
普通 SELECT 不加锁,但如果显式指定锁语法,会变成 当前读 并加锁(和隔离级别无关,RC/RR 下也生效):
-
共享锁(S 锁):
SELECT ... LOCK IN SHARE MODE允许其他事务读,禁止其他事务写(写需要 X 锁,会被 S 锁阻塞); -
排他锁(X 锁):
SELECT ... FOR UPDATE禁止其他事务读(加 S 锁)和写(加 X 锁),阻塞所有竞争锁的操作; -
举例:
-- 事务A(RR隔离级别) START TRANSACTION; SELECT * FROM user WHERE id=1 FOR UPDATE; -- 加行级X锁 -- 事务B执行 SELECT * FROM user WHERE id=1 LOCK IN SHARE MODE; 会阻塞 -- 事务C执行 UPDATE user SET name='test' WHERE id=1; 会阻塞 COMMIT; -- 释放X锁,B和C才会执行
2. MyISAM 存储引擎(淘汰,仅作了解)
MyISAM 不支持事务和行锁,只有 表级锁,普通 SELECT 会强制加 表级共享锁(S 锁) :
-
效果:
- 加 S 锁后,其他事务可以并发读(共享 S 锁);
- 其他事务的写操作(INSERT/UPDATE/DELETE)会被阻塞,直到所有读操作释放 S 锁;
- 反之,写操作会加表级排他锁(X 锁),阻塞所有读操作;
-
问题:并发性能极差(读阻塞写,写阻塞读),这也是 MyISAM 被 InnoDB 淘汰的核心原因;
-
举例:
-- 事务A(MyISAM表) SELECT * FROM user WHERE id=1; -- 加表级S锁 -- 事务B执行 UPDATE user SET name='test' WHERE id=1; 会阻塞,直到A执行完毕
三、关键概念补充(帮你理解本质)
-
快照读 vs 当前读:
- 快照读:普通
SELECT(RC/RR 级别),读历史版本,不加锁,依赖 MVCC; - 当前读:
SELECT ... FOR UPDATE/LOCK IN SHARE MODE、UPDATE/DELETE/INSERT、SERIALIZABLE 级别的普通SELECT,读最新版本,必须加锁;
- 快照读:普通
-
MVCC 的作用:InnoDB 通过 MVCC 让普通查询 “不加锁也能保证一致性”,是高并发的核心基础;
-
锁的粒度:InnoDB 支持行锁(粒度细,并发高),MyISAM 只有表锁(粒度粗,并发低)。
四、总结(实际开发重点)
-
日常开发用 InnoDB + RC/RR 隔离级别(默认配置):普通
SELECT不加锁,读写互不阻塞,并发性能最优; -
只有两种情况会加锁:
- 显式用
FOR UPDATE/LOCK IN SHARE MODE; - 隔离级别设为 SERIALIZABLE(极少用,仅需最高一致性场景);
- 显式用
-
避免用 MyISAM:其
SELECT加表锁,严重影响并发。
如果需要控制并发(比如避免 “超卖”),优先用 SELECT ... FOR UPDATE 显式加行锁,而非依赖隔离级别升级。
InnoDB 中,带索引条件的 SELECT/INSERT/UPDATE/DELETE 自动加行锁,行锁依赖索引,如果没有索引会升级为表锁
这个问题的核心原因是:InnoDB 的行锁本质是 “锁索引项”,而非直接锁数据行—— 只有通过索引才能精准定位到 “需要锁定的行”,无索引 / 索引失效时无法精准定位,只能通过全表扫描匹配目标行,此时为了保证事务隔离性和执行效率,InnoDB 会将行锁升级为表锁。
我们可以从「行锁的实现原理」「无索引时的执行困境」「升级表锁的必要性」三个层面,把原因拆解得更清楚:
一、先搞懂:InnoDB 行锁的本质是 “锁索引项”
InnoDB 是「聚簇索引」存储引擎(数据和主键索引叶子节点绑定存储),它的行锁设计完全依赖索引 ——锁定数据行的前提,是先通过索引找到对应的 “索引项”,再通过索引项关联到数据行。
简单说:
-
当你执行
UPDATE user SET age=20 WHERE id=1(id 是主键索引)时:- 数据库先通过主键索引
id快速定位到id=1对应的「主键索引项」; - 给这个「主键索引项」加行锁(行级排他锁 X 锁);
- 因为聚簇索引的特性,索引项和数据行是绑定的,锁了索引项就等同于锁了对应的数据行;
- 此时只锁
id=1对应的索引项,其他索引项(如id=2「id=3`)不受影响,这就是 “行锁” 的粒度。
- 数据库先通过主键索引
-
即使是二级索引(非主键索引),逻辑也一样:
- 比如
UPDATE user SET age=20 WHERE name='张三'(name 是二级索引); - 先通过二级索引
name找到name='张三'对应的「二级索引项」,加行锁; - 再通过二级索引项中存储的「主键 ID」,找到对应的「主键索引项」,也加行锁(避免其他事务通过主键修改该行);
- 最终还是通过索引项锁定目标行,粒度依然是行级。
- 比如
二、无索引 / 索引失效时:无法精准定位,只能全表扫描
如果 WHERE 条件没有索引,或者索引失效(比如用了 !=「OR「函数操作索引列等),数据库就失去了 “精准定位目标行” 的工具 —— 此时只能通过「全表扫描」逐行判断是否符合 WHERE 条件。
举个例子:UPDATE user SET age=20 WHERE age=30(age 无索引):
- 数据库无法通过索引快速找到
age=30的行,只能从表的第一行开始,逐行扫描、判断age是否等于 30; - 这个过程中,数据库无法提前知道哪些行符合条件,哪些不符合;
- 如果此时强行加行锁,会出现两个致命问题:
三、为什么必须升级为表锁?(InnoDB 的折中选择)
无索引时若不升级表锁,会导致「效率极低」或「数据不一致」,因此 InnoDB 选择升级为表锁,是平衡「隔离性」和「执行效率」的必然结果:
1. 问题 1:逐行加锁的开销极大,并发性能崩溃
如果全表扫描时逐行判断、逐行加锁(符合条件的行加锁,不符合的解锁),会产生海量的锁操作:
- 假设表有 100 万行,其中只有 10 行
age=30,数据库需要扫描 100 万行,执行 100 万次 “判断 - 加锁 / 解锁” 操作; - 锁操作是内核级别的开销,这种高频次的锁操作会让 SQL 执行效率暴跌,甚至拖垮数据库。
2. 问题 2:无法避免 “幻读”,破坏事务隔离性
即使承受逐行加锁的开销,也无法保证隔离性(比如 RR 级别下的幻读):
- 比如事务 A 执行
UPDATE user SET age=20 WHERE age=30(无索引),全表扫描到第 1000 行时,事务 B 插入了一条age=30的新行; - 事务 A 继续扫描后续行,会发现这条新插入的行也符合条件,需要加锁修改 —— 但这行是事务 A 扫描初期不存在的,属于 “幻读”,违背了 RR 隔离级别的要求;
- 若要避免幻读,需要给全表的 “间隙” 加锁(Next-Key Lock),但无索引时全表的间隙是无限的,根本无法实现。
3. 升级表锁的合理性:牺牲粒度,保障效率和隔离性
既然逐行加锁 “又慢又不安全”,InnoDB 只能选择「升级为表锁」:
- 一次性给整个表加排他锁(X 锁),直接禁止其他事务对表的任何修改(INSERT/UPDATE/DELETE)和加锁读(
FOR UPDATE等); - 虽然锁粒度变粗(影响并发),但避免了逐行加锁的巨大开销,同时保证了事务隔离性(不会出现幻读、脏写等问题);
- 这是 InnoDB 在 “无法精准定位行” 时的最优折中 —— 与其低效且不安全地逐行锁,不如高效且安全地锁全表。
总结:核心逻辑链
索引 → 精准定位索引项 → 锁索引项 = 锁数据行(行锁)
无索引/索引失效 → 无法定位索引项 → 只能全表扫描 → 逐行锁:低效+隔离性破坏 → 升级为表锁(平衡效率和隔离性)
简单说:索引是 InnoDB 行锁的 “定位地图”,没有地图就找不到具体要锁的行,只能把整个 “区域”(表)锁起来。这也是为什么实际开发中,一定要给查询 / 更新频繁的字段建索引 —— 不仅是为了查询提速,更是为了避免行锁升级为表锁,保障并发性能。