长事务阻塞业务的核心是锁未及时释放,因事务边界过大导致行锁/页锁长期持有;单事务包while循环是最大反模式,应分批更新(每批≤5000行)、主键推进分页、update top配索引、select for update紧贴update、禁用事务内网络/io操作,并监控清理幽灵事务。

长事务阻塞业务,核心问题不是SQL慢,而是锁没及时释放——事务边界切得太大,导致行锁/页锁长期持有,其他请求只能排队等。
为什么单事务包整个WHILE循环是最大反模式
常见错误:BEGIN TRANSACTION后用WHILE处理10万行,最后才COMMIT。哪怕每行UPDATE只要2ms,10万行也锁住近3分钟。
- 锁从第一行开始就捏着不放,后续所有并发请求在WHERE条件匹配的行上都会被阻塞
- MySQL/SQL Server都可能因扫描范围过大触发锁升级(行锁→页锁→表锁),影响面指数级扩大
- 一旦中间出错或客户端断连,事务悬空,变成“幽灵事务”,持续占用连接和MVCC清理资源
分批更新必须配对使用 UPDATE TOP (@batch_size) 和 COMMIT
每批控制在5000行以内,不是经验值,而是锁持有时间与并发吞吐的平衡点。
- 用主键推进式分页替代
OFFSET / FETCH:WHERE id > @last_id AND status = 'pending',避免越往后越慢 -
UPDATE TOP (@batch_size)必须配合索引——WHERE字段(如status)要有索引,否则TOP也无法限制扫描范围 - 每次
COMMIT后立即更新@last_id为本批最大id,否则会漏数据或重复处理 - 别在循环里做SELECT COUNT(*)校验总数,它本身就会加S锁,且在高并发下结果不可靠
SELECT FOR UPDATE 别提前拿,紧贴UPDATE前再加
很多人一进存储过程就SELECT ... FOR UPDATE,然后做日志、调外部服务、JSON解析——锁在这期间全程挂着。
- 把
SELECT ... FOR UPDATE挪到真正要UPDATE的语句前1~2行,中间不穿插任何非数据库逻辑 - 如果只是防重复提交,优先用
INSERT IGNORE或ON DUPLICATE KEY UPDATE,不依赖显式锁 - 加
FOR UPDATE NOWAIT,抢不到锁立刻报错1205,而不是让请求无限等待 - 事务内禁止执行网络请求、文件读写、复杂计算——这些操作不触发
innodb_lock_wait_timeout,但锁照挂不误
最容易被忽略的收尾动作:确认事务真结束了
应用异常退出、连接池复用未清理、调试时手动中断,都会留下trx_state = 'RUNNING'但trx_started是几分钟前的“幽灵事务”。它不争锁,但拖慢MVCC清理、阻塞DDL、占着连接资源。
上线前务必检查:SELECT * FROM information_schema.INNODB_TRX WHERE TIME_TO_SEC(NOW() - trx_started) > 30;——超过30秒的活跃事务,必须定位来源并加固回滚兜底。










