UPDATE未走索引时必然引发锁等待,因全表扫描导致每行加记录锁并激活间隙锁,等效锁整表;验证用EXPLAIN看type是否为ALL或index,修复需建合适索引、用主键游标分批、统一多表更新顺序、避免事务中混入非DB操作。

UPDATE没走索引时,锁等待几乎是必然的
只要WHERE条件字段没索引,或者索引因隐式转换(比如user_id是BIGINT却传入字符串'123')、函数包裹(如DATE(created_at) = '2026-06-01')而失效,InnoDB就会全表扫描——每扫一行加一个记录锁,间隙锁自动激活,效果等同于锁整张表。
验证方法很简单:EXPLAIN SELECT * FROM t WHERE your_condition,如果type列是ALL或index,就说明在扫全表。
- 先用
SHOW INDEX FROM table_name确认目标字段是否有可用索引,注意前缀索引是否覆盖查询长度 - 紧急修复可用
FORCE INDEX,但只是临时手段,长期必须修复执行计划 - 复合条件优先建联合索引,顺序按
WHERE中等值字段在前、范围字段在后(如(status, created_at))
分批更新不能靠LIMIT硬切,必须用主键范围游标
LIMIT在UPDATE里不提供稳定分片语义。当其他事务正在插入或删除数据时,“第2批100条”可能和上一批重叠或跳过某些记录——表面无错,但业务若依赖严格顺序处理,就会漏或重。
真正可控的方式是用主键做游标:每次UPDATE ... WHERE id BETWEEN ? AND ?,起始值取上一批的MAX(id),避免漏/重/重复扫描。
- 单批行数建议100–500,若更新涉及多表JOIN或大字段,缩到100以内更稳
- 务必显式
BEGIN+COMMIT,别依赖AUTOCOMMIT=1 - 每批后加
SLEEP(0.01)或SLEEP(0.1),缓解锁队列堆积和主从复制压力 - 别在事务里混用
SELECT FOR UPDATE或非DML语句,它们可能意外触发自动提交
多表UPDATE顺序不一致,死锁就是确定性发生
两个事务分别以不同顺序更新order和user表,InnoDB加锁就会形成环路。这不是概率问题,是确定性死锁源,占比超四成。
死锁日志里常看到TRANSACTION 123456789 locks record but not gap和另一事务waiting for this lock to be granted交替出现。
- 所有多表操作必须约定唯一顺序:按表名字母序(
order_item → order → user)或业务主次(订单流必须order → order_item → payment) - 禁止在代码里用
if/else动态拼接UPDATE顺序,例如不能根据type == 'refund'决定先更新user还是order - 每个表的访问都必须走主键或唯一索引,否则
SELECT FOR UPDATE可能升级为间隙锁,扩大冲突面
事务里混入非DB操作,锁就一直挂着
锁等待时间 = 事务开始到提交之间所有操作耗时。哪怕只更新100行,如果事务里混了HTTP调用、日志写入、循环计算,锁就一直挂着——其他事务一碰就Lock wait timeout exceeded。
- 把
SELECT FOR UPDATE后长时间处理再更新,改成先查ID列表、再算逻辑、最后UPDATE WHERE id IN ()单条提交 - 发消息、调第三方API、生成文件这些操作,一律移出事务块,改用异步队列或最终一致性补偿
- 高并发场景慎用
REPEATABLE READ:改用READ COMMITTED可减少间隙锁范围,降低锁冲突概率
最易被忽略的点是:即使你写了LIMIT 100,如果EXPLAIN显示type: ALL,MySQL仍会全表扫描+全表加锁,等于事实锁表。


















