定时清理变“长事务炸弹”是因为delete全表30%且持续写入时未分批,若分批脚本缺失失败重试与进度记录,中断后重复执行将导致漏删或唯一键冲突。

为什么定时清理会变成“长事务炸弹”
直接在定时任务里跑 DELETE FROM t WHERE created_at ,看似简单,实则高危。它会把所有匹配行锁住、写入 undo log、撑爆 binlog、拖慢主从同步——尤其当删的是百万级表时,一个语句就卡住十几秒甚至几分钟,其他业务 DML 全部排队等锁。
必须分批删,且每批独立提交
不分批 = 长事务 = 阻塞源。关键不是“要不要分”,而是“怎么分才安全可控”:
-
LIMIT是底线:单次最多删 5000 行,大表建议 1000–2000;超过这个数,innodb_log_file_size容易打满,回滚段压力陡增 - 别依赖存储过程自动循环:MySQL 存储过程里
WHILE循环若没加COMMIT或设AUTOCOMMIT=1,整个循环仍在一个事务里——等于换汤不换药 - 推荐用外部脚本驱动:比如 cron 调用
mysql -e "DELETE FROM t WHERE ... LIMIT 1000",每次都是新连接、新事务、自然提交 - 加
SLEEP不是可选:每批后DO SLEEP(0.1)或 shell 中sleep 0.1,能明显缓解 IO 和锁竞争,尤其在高并发时段
WHERE 条件必须走索引,否则全表扫描+锁表
created_at 字段没索引?那 DELETE WHERE created_at 就是隐式全表扫描,不仅慢,还会对整张表加意向锁,阻塞所有 DML。
验证方法:EXPLAIN DELETE FROM t WHERE created_at ,看 <code>type 是否为 range 或更好,key 是否显示用了索引。
常见坑:
- 用
BETWEEN或字符串拼接时间(如CONCAT('2024-', '01', '-01')),导致索引失效 - 字段类型是
DATETIME但传入字符串未加引号,触发隐式转换 - 时区不一致:应用层用东八区时间生成条件,但 MySQL server 时区是 UTC,查不到数据或误删
删完不优化,空间不释放;乱优化,反而更卡
大批量删除后,InnoDB 只是标记页为空闲,不会立刻归还磁盘空间。但 OPTIMIZE TABLE 不是万能解药:
- 它会重建整表,期间锁表(
ALGORITHM=INPLACE在某些版本/场景下也不完全免锁) - 小表可以跑,大表慎用——一次
OPTIMIZE卡住业务半小时很常见 - 更稳妥做法:监控
data_free值(查INFORMATION_SCHEMA.TABLES),仅当它 > 表总大小 30% 且后续有持续写入压力时,再考虑ALTER TABLE t ENGINE=InnoDB
真正容易被忽略的点:分批脚本没加失败重试和进度记录。删到一半出错(比如网络中断、权限变更),下次又从头开始,重复删已删过的数据——要么漏删,要么触发唯一键冲突。得靠外部状态文件或单独记录表来跟踪 last_id / last_time。











