UPDATE走索引需确保WHERE条件字段有索引,联合索引遵循最左匹配,避免函数包裹导致索引失效,否则会全表扫描并锁表。

确认UPDATE是否走索引,避免全表扫描锁表
没走索引的 UPDATE 在 InnoDB 中会退化为全表扫描,每行加记录锁+间隙锁,等效锁整张表。现象是 EXPLAIN 显示 type: ALL 或 rows_examined 接近表总行数,SHOW PROCESSLIST 里大量连接卡在 Locked 状态。
实操建议:
- 执行
EXPLAIN FORMAT=JSON SELECT * FROM t WHERE your_condition,重点看key字段是否非NULL、rows_examined是否合理 - WHERE 条件字段必须建索引;联合索引注意最左匹配,比如有
(status, created_at),但写WHERE created_at > '2026-01-01'就用不上 - 删掉函数包裹:把
WHERE DATE(create_time) = '2026-08-01'改成WHERE create_time >= '2026-08-01' AND create_time - 警惕隐式类型转换:
user_id是BIGINT,却传字符串'123',索引直接失效
分批更新:用主键范围切分,别用 LIMIT OFFSET
LIMIT 在 UPDATE 中不保证幂等分页,高并发下容易漏行或重复更新;OFFSET 还会导致 MySQL 重复扫描前面的行,锁范围不可控。真正可控的是基于主键的连续范围切分。
实操建议:
- 先查一批 ID:
SELECT id FROM t WHERE condition ORDER BY id LIMIT 500,取MIN(id)和MAX(id) - 下一批用
WHERE id > ? AND id ,避免 <code>OFFSET扫描开销 - 单批控制在 100–500 行之间;若涉及多表 JOIN 或大字段,建议缩到 100 以内
- 每批后显式
COMMIT,并加SLEEP(0.01)缓解锁争抢和 CPU 压力
缩短事务生命周期:移出非DB操作
锁等待时间 = 事务从 BEGIN 到 COMMIT 的全部耗时。哪怕只更新 100 行,如果事务里混了 HTTP 调用、日志写入、循环计算,锁就一直挂着,别人全得排队。
实操建议:
- 把消息发送、第三方 API 调用、文件生成、Redis 写入等全部移出事务块,改用异步队列或最终一致性补偿
- 校验逻辑(如“库存是否充足”)尽量提到事务外;必须查库校验时,用
SELECT ... LOCK IN SHARE MODE显式控制,别依赖隐式快照 - Django 中检查是否误用
@transaction.atomic包裹了非 DB 操作;Spring 中确认@Transactional的propagation没意外嵌套 - 数据库侧设
innodb_lock_wait_timeout = 10,配合应用层捕获Lock wait timeout exceeded错误做快速失败或重试
统一多表操作顺序,防死锁链
两个事务分别以不同顺序更新 order 和 user 表,InnoDB 加锁就会形成环路——这不是概率问题,是确定性死锁源,占比超四成。死锁日志里常见 TRANSACTION 123456789 locks record but not gap 和另一事务 waiting for this lock 交替出现。
实操建议:
- 所有业务逻辑中,对多表/多行的修改必须按固定顺序:按表名字母序(
order_item → order → user),或按业务主次(订单流必须order → order_item → payment) - 禁止在代码里用
if/else动态拼接 UPDATE 表顺序,例如不能根据type == 'refund'决定先更新user还是order - 每个表的访问都必须走主键或唯一索引,否则
SELECT FOR UPDATE可能升级为间隙锁,扩大冲突面 - 紧急时可用
FORCE INDEX强制走索引,但只是临时手段;长期要修复执行计划和索引设计
锁竞争不是性能问题,是事务边界、索引有效性、执行路径三者耦合的结果。最容易被忽略的是:你以为在优化 SQL,其实真正卡住你的,是上一个忘了 COMMIT 的长事务,或者 ORM 自动开启却没关的隐式事务。

















