必须采用主键范围分批+显式小事务+短延时策略,因InnoDB按扫描范围加锁且锁持至提交,大事务引发间隙锁堆积、MVCC版本链暴涨及读阻塞。

能,但必须放弃“一条UPDATE干完”的幻想,改用主键范围分批 + 显式小事务 + 短延时的组合策略。任何试图靠 LIMIT 或 IGNORE 单次搞定大表更新的做法,在 MySQL 8.0 中依然会触发间隙锁堆积、MVCC 版本链暴涨,最终卡住 SELECT。
为什么 UPDATE 大表会阻塞读请求
InnoDB 的行锁不是“只锁命中的行”,而是按扫描范围加锁。即使你写 WHERE id IN (1,2,3),只要执行计划没走索引(EXPLAIN 显示 type: ALL),就会全表扫描+全表加锁;即使走了索引,大事务也会让间隙锁长期持有,其他事务一碰就报 Lock wait timeout exceeded 或卡在 Waiting for table metadata lock。更隐蔽的是 MVCC:大事务不提交,旧版本数据无法被 purge 线程清理,SELECT 越来越慢,甚至超时。
必须用主键范围切分,禁用 OFFSET
分批更新的核心是让每次 UPDATE 都精准定位、快速结束、立刻释放锁。不能依赖 LIMIT 1000 OFFSET 5000 —— 它越往后扫描越慢,且可能漏数据或重复更新。
- 先查边界:
SELECT MIN(id), MAX(id) FROM t WHERE status = 0 - 每次更新用:
WHERE id BETWEEN ? AND ?,步长建议 100–500(单行越宽,步长越小) - 下一批起点必须是上一批
MAX(id)+ 1,不能靠OFFSET推算 - 务必确认
id是主键或唯一索引,否则JOIN或范围条件会失效
每批必须显式 BEGIN/COMMIT,别信 AUTOCOMMIT=1
显式事务控制是释放锁的唯一可靠方式。AUTOCOMMIT=1 下,每条语句自动提交,看似安全,但无法控制批次节奏,锁释放太“急”,反而加剧锁队列抖动;而把所有批次包在一个事务里,又等于回到大事务陷阱。
安装、修复、更新或打包一个基于 Python 和 PyMySQL 实现的 Windows `mysql` 命令。适用于用户希望在 PowerShell/CMD 中输入 `mysql` 时重定向到 Python 实现、需要交互式输入 MySQL 连接信息、需要同步到当前用户本地 bin 目录,或需要将该能力封装成可公开复用的 Codex skill。
- 每批开头写
BEGIN,更新后立刻COMMIT - 用
ROW_COUNT()判断是否还有数据可处理,避免空跑 - 批次间加
SLEEP(0.05)或SLEEP(0.1),缓解主从复制压力和锁竞争 - 严禁在事务内混用
SELECT FOR UPDATE或非 DML 语句,它们可能意外触发自动提交
WHERE 条件没走索引时怎么办
这是最常被忽略的致命点:加了 LIMIT 也救不了全表扫描。分批的前提是每次都能快速定位下一批起点。
- 优先加索引:
ALTER TABLE t ADD INDEX idx_status_created (status, created_at)(注意评估加索引期间的锁表风险) - 若不能加索引,改用主键子查询(MySQL 8.0+ 支持):
UPDATE t SET status = 1 WHERE id IN (SELECT id FROM (SELECT id FROM t WHERE create_time > '2023-01-01' LIMIT 1000) AS tmp) - 绝对不要写
WHERE create_time > '2023-01-01' ORDER BY id LIMIT 1000——ORDER BY+LIMIT在无索引时仍是全表扫描
真正难的不是写对 SQL,而是判断每批该取多大、SLEEP 该设多少、以及如何验证锁是否真的释放了。这些参数必须结合你的单行大小、磁盘 I/O、binlog 格式(ROW 还是 STATEMENT)实测调整,没有通用值。










