直接拆成小事务,每批100–500行、用主键范围切分、显式BEGIN/COMMIT可避免长锁;因InnoDB按扫描范围加锁,大事务致间隙锁扩大、锁持有至提交,引发阻塞和超时。

直接拆成小事务,每批控制在 100–500 行、用主键范围切分、显式 BEGIN/COMMIT,别信 LIMIT 单独能解决问题。
为什么 UPDATE 大批量数据会卡住其他查询
MySQL 的行锁不是“只锁命中的行”,而是按扫描范围加锁——哪怕你 WHERE id IN (1,2,3),只要执行计划走全表或索引范围过大,InnoDB 就可能升级为间隙锁(lock_mode X locks gap before rec)甚至临键锁,锁住一大片索引区间。更关键的是,锁会一直持有到事务提交,而大事务意味着长锁期、高锁密度、大量 TRX_ROWS_LOCKED,其他事务一碰就 Waiting for table metadata lock 或直接报 Lock wait timeout exceeded。
常见错误现象包括:SHOW PROCESSLIST 里一堆 Updating 状态;INFORMATION_SCHEMA.INNODB_TRX 显示 trx_state = 'RUNNING' 且 trx_started 几分钟前就开始了;performance_schema.data_locks 里能看到大量 lock_status = 'waiting'。
怎么安全地分批更新:主键范围驱动 + 显式事务控制
核心是让每次 UPDATE 都能精准定位、快速结束、释放锁。不能靠 LIMIT 混过去,必须用主键(或唯一索引)做游标,避免 OFFSET 扫描和漏/重数据。
- 先查出待更新的最小和最大主键:
SELECT MIN(id), MAX(id) FROM t WHERE status = 0 - 每次更新用
WHERE id BETWEEN ? AND ?,步长建议 100–500(不是硬指标,要结合单行大小和磁盘负载测) - 务必显式
BEGIN开启事务,执行完UPDATE后立刻COMMIT,不要依赖AUTOCOMMIT=1 - 每批后加
SLEEP(0.01)或SLEEP(0.1),缓解锁队列堆积和主从复制压力 - 别在事务里混用
SELECT FOR UPDATE或非 DML 语句,它们可能意外触发自动提交
WHERE 条件没走索引时怎么办
这是最常被忽略的致命点:即使你写了 LIMIT 100,如果 EXPLAIN 显示 type: ALL,MySQL 仍会全表扫描+全表加锁,等于事实锁表。
可行解法只有两个:
- 临时建索引:
CREATE INDEX idx_status ON t(status)(DDL 期间表可读不可写,但比锁表几小时好) - 应用层两步走:先
SELECT id FROM t WHERE status = 0 ORDER BY id LIMIT 500拿 ID 列表,再UPDATE t SET ... WHERE id IN (1,2,3,...)(IN列表别超 1000 项)
别试 FORCE INDEX——没对应索引时,它只是语法通过,执行计划仍是全扫。
容易踩的坑和监控要点
分批本身不难,难的是边界干净、锁真释放、连接状态不残留。
- 每批执行后必须检查
ROW_COUNT(),为 0 就停,否则可能无限循环 - 运行前确认
SELECT @@autocommit,必要时SET autocommit = 0,防止 ORM 或连接池偷偷提交 - 别调大
innodb_lock_wait_timeout来“掩盖”问题——它只让报错延迟,锁还在那里堵着 - 重点盯
performance_schema.data_locks和INNODB_TRX,特别是trx_rows_locked和trx_started时间差
真正卡住你的往往不是数据量,而是某一批次因索引失效或主键跳空导致扫描放大,或者事务没真正提交却以为结束了。


















