update主键本质是“删旧+插新”而非原地修改,需两次行锁、触发间隙锁,并强制全量重写所有二级索引,导致锁范围指数级扩大、极易死锁。

UPDATE主键实际是“删旧+插新”,不是原地修改
InnoDB 从不就地更新主键值。执行 UPDATE t SET id = 101 WHERE id = 1 时,引擎真实行为是:先在聚簇索引中定位并标记旧记录为删除(加 X 锁),再在新主键位置插入一条全新记录(加 X 锁 + Gap Lock)。这个过程不可原子化,中间态对其他事务可见。
这意味着:哪怕只改一行,也至少触发两次行级锁;若新位置被间隙锁封锁,还会引发等待;若表上有二级索引,每条索引项都得重写——因为所有二级索引叶子节点都存着主键值,必须同步更新。
二级索引全量重写会指数级扩大锁范围
假设表有 INDEX idx_status_created (status, created_at),你执行 UPDATE t SET id = id * 2 WHERE status = 'pending',InnoDB 实际要走三步:
- 扫描
idx_status_created找出所有status = 'pending'的记录 → 加 S 或 X 锁 - 对每条匹配记录,用其叶子节点里的旧
id去聚簇索引查原行 → 可能再加 X 锁 - 用新
id重构该二级索引项 → 再次触发聚簇索引定位和加锁
这种“索引链式加锁”会让锁范围远超 SQL 表面所见。尤其当二级索引重复值多、基数大时,极易与其他事务形成环形等待,SHOW ENGINE INNODB STATUS\G 中常看到大量 LOCK WAIT,且锁住的不只是目标行,而是整段索引区间。
WHERE条件没走索引 = 隐式全表加锁
如果 WHERE 条件无法命中索引,比如 UPDATE t SET id = id + 1 WHERE name LIKE '%abc%',InnoDB 在 RR 隔离级别下会:
- 全表扫描每行,对**所有扫描到的记录加 X 锁**
- 对**所有扫描经过的间隙加 Gap Lock**(防止幻读)
- 不同事务因缓冲池状态、B+树遍历路径差异,可能以相反顺序加锁 → 构成经典死锁四元组
此时即使只改主键,效果等同于隐式表锁。高并发下,性能退化剧烈,且 TRANSACTIONS 段会显示大量阻塞,锁对象远不止你要更新的那一行。
真正需要“重编号”的场景极少,业务逻辑往往误用了主键
绝大多数所谓“更新主键”,本质是把主键当成了可变业务标识。更安全、低开销的做法是:
- 保留自增
id作为物理主键,永不更新 - 新增一个带业务含义的唯一字段(如
business_code),承载可变标识逻辑 - 所有对外暴露、前端传参、API 路由都基于该业务字段,而非
id
主键变更带来的锁扩散、死锁风险、二级索引重建开销,远超业务层多维护一个字段的成本。真正踩坑的,往往是那个以为“只是改个数字”的瞬间。











