mysql update卡表主因是where未走索引导致锁全表,或大范围更新长期持锁;应确保索引命中、分批提交、加sleep限流、避开高峰,并优先用pt-archiver替代手写脚本。

UPDATE 为什么会让整个表卡住
MySQL 的 UPDATE 默认走行锁,但前提是能用上索引。如果 WHERE 条件没走索引,InnoDB 会升级为表级锁(更准确说是锁住所有聚簇索引记录,效果等同于锁表)。另外即使走了索引,若更新范围太大(比如几百万行),事务长时间持有锁,也会阻塞其他读写。
常见错误现象:SHOW PROCESSLIST 看到大量 Waiting for table metadata lock 或 Updating 状态卡住;业务接口超时、主从延迟飙升。
- 检查是否命中索引:用
EXPLAIN UPDATE ...(实际执行前加EXPLAIN关键字,MySQL 8.0.19+ 支持) - 避免在大表上直接
UPDATE ... WHERE created_at 这类无索引时间范围更新 - 注意隐式类型转换:比如
WHERE user_id = '123'(字段是INT),会导致索引失效
怎么切分小批量 UPDATE(带安全边界)
核心是用主键或唯一有序字段做游标分页,每次只处理几千行,控制事务体积和锁持有时间。不能用 LIMIT OFFSET,因为数据变动后偏移会错位。
推荐方式:按主键递增范围扫描
- 第一次查最小 ID:
SELECT MIN(id) FROM t WHERE status = 0 - 循环执行:
UPDATE t SET status = 1 WHERE id BETWEEN ? AND ? AND status = 0 - 每次取
1000 ~ 5000行,具体看单行数据大小和 QPS 压力 - 务必带上原业务条件(如
status = 0),防止重复更新
示例逻辑(伪代码):
last_id = 0
while True:
rows = SELECT id FROM t WHERE id > last_id AND status = 0 ORDER BY id LIMIT 5000
if not rows: break
UPDATE t SET status = 1 WHERE id IN (/* rows */) AND status = 0
last_id = rows[-1].id
事务提交频率和 sleep 控制节奏
每批 UPDATE 后立刻 COMMIT,否则锁一直不释放。但也不能太激进——高频提交会压垮 binlog 和磁盘 I/O,尤其在主从架构下容易拖慢复制线程。
实操建议:
- 每批执行完必须
COMMIT,禁止包在一个大事务里 - 每批之间加
SLEEP(0.1)(100ms),给其他查询喘息机会;线上可调到0.05 ~ 0.2间平衡速度与干扰 - 避开业务高峰执行;用
SELECT @@innodb_row_lock_time_avg监控锁等待恶化趋势 - 不要用存储过程封装循环——出错难中断、日志难追踪、DBA 不好介入
用 pt-archiver 替代手写脚本的适用场景
当更新逻辑简单(单表、单条件、无复杂 JOIN)、且你不想维护调度和重试逻辑时,pt-archiver 是更稳的选择。它内置游标分片、失败重试、限流、进度打印,比手写 Python/Shell 更少出错。
典型命令:
pt-archiver \ --source h=localhost,D=app,t=user \ --where "status = 0 AND created_at <p>注意点:</p>
-
--no-delete是关键,否则默认会删源数据 -
--bulk-insert加速写入,但要求目标列允许 NULL 或有默认值 - 首次务必加
--dry-run看它生成的 SQL 是否符合预期 - 不支持跨库 JOIN 更新,复杂逻辑还得自己写
真正麻烦的不是切分本身,而是确认每一批更新都满足业务幂等性——比如状态机流转、金额累加、外部服务回调,这些没法靠 WHERE status = 0 完全兜住。得在应用层补校验或加分布式锁。











