加了limit仍锁表,根本原因是事务未正确提交:autocommit=0时,多轮delete堆积在长事务中持续持锁;且无order by导致扫描顺序不确定而漏删/重复删;若where条件未走索引,则每批仍是全表扫描加锁。

为什么加了 LIMIT 还会锁表?
根本不是 LIMIT 没起作用,而是你没关好事务边界。MySQL 默认 autocommit=1,每条带 LIMIT 的 DELETE 是独立小事务;但一旦你执行过 START TRANSACTION 或应用层开启了未提交的事务(比如 Spring 的 @Transactional 未退出),后续所有 DELETE 就堆在一个长事务里——锁不释放、undo log 持续膨胀、innodb_log_waits 开始飙升。
查当前状态:SELECT @@autocommit;,值为 0 就必须手动 COMMIT,哪怕只删 100 行。
- 连接池复用时,尤其要确认是否复用了已开启事务的连接
- 存储过程中必须显式
COMMIT,不能依赖默认行为 - 脚本循环里每次
DELETE后必须跟COMMIT,不能只在末尾提交
ORDER BY id 不是可选项,是必选项
不加 ORDER BY 的 DELETE FROM t WHERE status = 0 LIMIT 1000 是危险操作:InnoDB 不保证扫描顺序,尤其并发写入下,同一批可能反复删 id 在 100~200 区间的行,跳过 id > 5000 的行,导致漏删或重复删。
正确做法是锚定单调字段(如自增 id 或带索引的 created_at):
DELETE FROM logs WHERE create_time 12345 ORDER BY id LIMIT 1000- 每次执行后记录最后删掉的
id值(比如SELECT MAX(id)),作为下一轮id > @last_id的起点 - 避免用
OFFSET分页,它越往后越慢(MySQL 仍需跳过前面所有行)
索引失效会让分批变成“伪分批”
WHERE 条件没走索引,LIMIT 就救不了你——每轮都在全表扫描找 1000 行,性能比单次大删还差。看 EXPLAIN 的 type 字段,如果是 ALL 或 rows 接近表总行数,说明没走索引。
- 过滤字段基数低(如
status只有 0/1)时,必须建联合索引,例如INDEX idx_status_id (status, id) - 避免在索引列上做计算:
DATE(create_time) = '2025-01-01'会让索引失效,改用create_time >= '2025-01-01' AND create_time - 别用
LIKE '%xxx'开头,类型转换(如字符串字段跟数字比较)也会绕过索引
READ COMMITTED 能少锁一大半
线上还在用默认的 REPEATABLE READ?那 DELETE 会加 next-key lock(行锁 + 间隙锁),哪怕只删 1 行,也可能锁住相邻空闲区间,引发更多等待。改成 READ COMMITTED 后,InnoDB 只对实际命中的行加 record lock,不加 gap lock。
执行前设会话级隔离级别:SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-
RC下不可重复读是允许的,日志清理、归档类场景完全可接受 - 注意:不要全局改
GLOBAL级别,只针对当前会话生效即可 - 如果业务强依赖 RR 的一致性语义(比如金融对账),那就得靠更严格的锚点控制和索引优化来兜底
DELETE 后是否真的释放了锁——这取决于你有没有在正确的时机 COMMIT,以及索引是否真正在起作用。











