InnoDB 中,带索引条件的 SELECT/INSERT/UPDATE/DELETE 自动加行锁,行锁依赖索引,如果没有索引会升级为表锁

76 阅读15分钟

mysql中的普通的update,delete,INSERT语句是否会加锁

MySQL 中普通的 UPDATEDELETEINSERT 语句都会加锁,且核心锁行为依然由「存储引擎」和「索引」决定 —— 写操作(增删改)需要保证数据一致性,避免并发冲突(如脏写、丢失更新),因此默认会加排他锁(X 锁) (最严格的锁,禁止其他事务读锁 / 写锁竞争)。

以下是分存储引擎、分语句的详细拆解,重点聚焦实际开发常用的 InnoDB 引擎:

一、核心结论先明确

存储引擎语句类型是否加锁锁类型(默认)关键依赖
InnoDB(主流)UPDATE/DELETE行级排他锁(X 锁)依赖索引(无索引 / 失效 → 表锁)
InnoDBINSERT隐式排他锁 + 间隙锁主键 / 唯一索引防冲突,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 执行完毕
    

三、关键注意事项(实际开发必看)

  1. 锁的释放时机:InnoDB 中,写操作的锁(行锁 / 表锁 / 间隙锁)都是「事务级锁」,只有事务提交(COMMIT)或回滚(ROLLBACK)后才会释放;若事务长时间不提交,会导致锁等待超时(默认超时时间由 innodb_lock_wait_timeout 控制,默认 50 秒)。

  2. 索引是行锁的前提:和 UPDATE/DELETE 一样,INSERT 的间隙锁也依赖索引(尤其是主键 / 唯一索引);若插入的字段无索引,可能导致间隙锁范围扩大,影响并发。

  3. 隔离级别的影响

    • RR(默认):INSERT 会加间隙锁 / Next-Key Lock,防幻读,但锁粒度略粗;
    • RC:INSERT 仅加隐式行锁,间隙锁失效,可能出现幻读,但并发更高;
  4. 避免长事务:写操作若在长事务中执行,锁会长期持有,容易引发锁等待、死锁(比如两个事务互相持有对方需要的锁),实际开发中要尽量缩短事务周期。

四、select语句查询是否会有锁

MySQL 中普通 SELECT 语句是否加锁,核心取决于存储引擎和事务隔离级别,其中 InnoDB(默认事务型引擎)和 MyISAM(传统非事务型引擎)的行为差异极大,以下是详细拆解:

一、核心结论先明确
存储引擎事务隔离级别(InnoDB)普通 SELECT 是否加锁关键说明
InnoDB(主流)RC(读已提交)/ RR(可重复读,默认)/ RU(读未提交)不加锁(快照读)基于 MVCC 实现 “非锁定读”,不阻塞读写
InnoDBSERIALIZABLE(串行化)加锁(共享锁)强制当前读,阻塞写操作
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执行完毕
    
三、关键概念补充(帮你理解本质)
  1. 快照读 vs 当前读

    • 快照读:普通 SELECT(RC/RR 级别),读历史版本,不加锁,依赖 MVCC;
    • 当前读:SELECT ... FOR UPDATE/LOCK IN SHARE MODE、UPDATE/DELETE/INSERT、SERIALIZABLE 级别的普通 SELECT,读最新版本,必须加锁;
  2. MVCC 的作用:InnoDB 通过 MVCC 让普通查询 “不加锁也能保证一致性”,是高并发的核心基础;

  3. 锁的粒度:InnoDB 支持行锁(粒度细,并发高),MyISAM 只有表锁(粒度粗,并发低)。

四、总结(实际开发重点)
  1. 日常开发用 InnoDB + RC/RR 隔离级别(默认配置):普通 SELECT 不加锁,读写互不阻塞,并发性能最优;

  2. 只有两种情况会加锁:

    • 显式用 FOR UPDATE/LOCK IN SHARE MODE
    • 隔离级别设为 SERIALIZABLE(极少用,仅需最高一致性场景);
  3. 避免用 MyISAM:其 SELECT 加表锁,严重影响并发。

如果需要控制并发(比如避免 “超卖”),优先用 SELECT ... FOR UPDATE 显式加行锁,而非依赖隔离级别升级。

InnoDB 中,带索引条件的 SELECT/INSERT/UPDATE/DELETE 自动加行锁,行锁依赖索引,如果没有索引会升级为表锁

这个问题的核心原因是:InnoDB 的行锁本质是 “锁索引项”,而非直接锁数据行—— 只有通过索引才能精准定位到 “需要锁定的行”,无索引 / 索引失效时无法精准定位,只能通过全表扫描匹配目标行,此时为了保证事务隔离性和执行效率,InnoDB 会将行锁升级为表锁。

我们可以从「行锁的实现原理」「无索引时的执行困境」「升级表锁的必要性」三个层面,把原因拆解得更清楚:

一、先搞懂:InnoDB 行锁的本质是 “锁索引项”

InnoDB 是「聚簇索引」存储引擎(数据和主键索引叶子节点绑定存储),它的行锁设计完全依赖索引 ——锁定数据行的前提,是先通过索引找到对应的 “索引项”,再通过索引项关联到数据行

简单说:

  1. 当你执行 UPDATE user SET age=20 WHERE id=1(id 是主键索引)时:

    • 数据库先通过主键索引 id 快速定位到 id=1 对应的「主键索引项」;
    • 给这个「主键索引项」加行锁(行级排他锁 X 锁);
    • 因为聚簇索引的特性,索引项和数据行是绑定的,锁了索引项就等同于锁了对应的数据行;
    • 此时只锁 id=1 对应的索引项,其他索引项(如 id=2「id=3`)不受影响,这就是 “行锁” 的粒度。
  2. 即使是二级索引(非主键索引),逻辑也一样:

    • 比如 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 行锁的 “定位地图”,没有地图就找不到具体要锁的行,只能把整个 “区域”(表)锁起来。这也是为什么实际开发中,一定要给查询 / 更新频繁的字段建索引 —— 不仅是为了查询提速,更是为了避免行锁升级为表锁,保障并发性能。