InnoDB的UPDATE加锁范围比预期大,因其行锁实际锁定索引记录;若需回表则对主键加X锁,即使WHERE仅命中一条二级索引;覆盖索引须同时覆盖WHERE、SET及ORDER BY字段;非唯一二级索引等值更新会加Next-Key Lock;分批UPDATE需配合覆盖索引与显式COMMIT才安全。

UPDATE加锁范围为什么比你想象的大
因为InnoDB的行锁不是锁“物理行”,而是锁“索引记录”。只要UPDATE语句需要回表(即访问聚簇索引),就会对主键索引上的对应行加X锁——哪怕WHERE只命中一条二级索引,SET字段又不在该索引里,照样触发回表、扩大锁范围。常见现象是:SHOW ENGINE INNODB STATUS\G里看到大量lock_mode X locks rec but not gap,但实际阻塞了不该阻塞的事务。
覆盖索引必须同时覆盖WHERE、SET和ORDER BY字段
覆盖索引不是“有索引就行”,而是让优化器全程不碰聚簇索引。对UPDATE生效的覆盖索引,需满足:
- WHERE条件字段必须是索引最左前缀(如
WHERE user_id = ? AND status = ?,索引就得是(user_id, status, ...)) - 所有
SET字段必须包含在索引中(如SET status = 'done', updated_at = NOW(),索引末尾就得有updated_at) - 如果UPDATE带
ORDER BY或LIMIT,排序字段也得进索引,否则可能退化为全扫描 - MySQL 8.0.13+可用
INCLUDE语法补非索引列,但老版本必须显式写入(如(user_id, status, updated_at)已够用,不必再加id)
非唯一索引下,等值UPDATE仍会加间隙锁
即使WHERE走的是二级索引,只要该索引不是UNIQUE,InnoDB就会加Next-Key Lock——锁住匹配值及其前后间隙。比如INDEX idx_status (status),执行UPDATE t SET x=1 WHERE status = 'pending',可能锁住('done', 'pending']和('pending', 'success')两个间隙,导致其他事务插入新pending记录被阻塞。
解决办法只有两个:
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
- 把高频等值字段显式定义为
UNIQUE(哪怕业务上不强制唯一,MySQL也认这个声明) - 改用主键做定位:
SELECT id FROM t WHERE status = 'pending' LIMIT 1000→ 拿到id列表后再UPDATE t SET ... WHERE id IN (...),此时只加record lock
分批UPDATE必须配合覆盖索引才真正安全
单纯加LIMIT不解决锁范围问题。例如UPDATE t SET status='done' WHERE status='pending' LIMIT 1000,若status无索引,仍是全表扫描加锁;若有索引但没覆盖updated_at,仍要回表加主键锁。
正确姿势是:
- 建联合索引:
ALTER TABLE t ADD INDEX idx_status_updated (status, updated_at) - 分批语句写成:
UPDATE t SET status='done', updated_at=NOW() WHERE status='pending' ORDER BY id LIMIT 1000 - 每次执行后立刻
COMMIT,避免长事务堆积锁 - 用
ROW_COUNT()判断是否还有数据,别依赖固定循环次数
最容易被忽略的一点:覆盖索引能减少锁行数,但不能消除锁竞争本身。当多个事务更新同一索引值范围(比如都查status='pending'),冲突照旧发生——这时候得靠应用层限流或错峰,索引救不了。

















