非索引列更新必须加事务,否则会因全表扫描在主键上加大量X锁,导致阻塞、死锁和并发写入瘫痪;RC隔离级别也无法避免锁扩散,长事务更会使锁长期滞留。

非索引列更新在 MySQL 中开启事务不是“要不要”的问题,而是“不加事务就更危险”的问题——它本身不会因事务而变安全,但没事务会直接放大锁范围、阻塞面和死锁风险。
非索引列 WHERE 条件触发全表扫描加锁
当执行 UPDATE t SET c2 = 'x' WHERE c3 = 4,而 c3 没有索引时,InnoDB 必须逐行扫描主键索引。每扫描一行,就在该行的主键记录上加 X(排他)锁;即使最终不满足 c3 = 4,锁也已加上——直到语句执行结束才释放(RC 隔离级别下 Server 层会释放不匹配行的锁,但 InnoDB 存储引擎层仍需先加再放,中间存在窗口期)。
- 后果:其他事务对任意被扫描过的行做
UPDATE或DELETE都会被阻塞 - 特别危险的是:
INSERT不受影响,但UPDATE已被锁住的行(哪怕c3值不同)也可能被卡住,因为锁在主键上,跟c3值无关 - 若扫描了 10 万行,就等于临时给这 10 万行主键都上了 X 锁——并发写入几乎瘫痪
READ COMMITTED 下仍无法避免锁扩散
有人误以为换成 READ COMMITTED 就能规避问题,其实不然。虽然 RC 级别下不加 GAP 锁、且 Server 层会释放不匹配行的锁,但「加锁 → 判断 → 释放」这个过程仍存在竞争窗口:
- 两个并发事务同时执行
UPDATE ... WHERE c3 = 4(c3无索引),可能各自扫描到同一行并加锁,随后又各自释放——但中间短暂重叠可能引发死锁 - MySQL 的锁释放不是原子操作,尤其在大结果集场景下,锁持有时间拉长,超时和等待队列堆积概率陡增
-
SHOW ENGINE INNODB STATUS中常看到lock_trx_id和lock_mode X locks rec but not gap大量出现,就是典型征兆
事务未提交导致长时间锁滞留
如果这类非索引更新被包裹在长事务中(例如前面还有 SELECT、或后面还有其他 DML),那它加的那些主键 X 锁会一直持有着,直到事务 COMMIT 或 ROLLBACK:
- 一个未提交的
UPDATE ... WHERE c3 = 4可能让几十万行持续不可写,影响远超语句本身意图 - 配合
autocommit = OFF或显式BEGIN却忘记COMMIT,极易演变成“隐形锁霸占者” -
information_schema.innodb_trx中trx_started时间戳明显早于当前时间,且trx_query是类似UPDATE t SET ... WHERE c3 = ?,基本可定位为元凶
真正要警惕的,从来不是“是否开启事务”,而是“有没有意识到非索引条件在更新时等于主动申请全表主键锁”。加事务只是让行为可控、可回滚;不加事务反而会让失败更难追溯、恢复更难操作。


















