根本原因是事务边界设计不当导致锁长期持有;应分批处理(每批≤5000行)、索引优化WHERE条件、推迟SELECT FOR UPDATE至UPDATE前、禁用事务内耗时操作、确保事务及时结束。

SQL存储过程里出现长事务阻塞,根本原因不是SQL写得慢,而是事务边界没切好——锁一直不放,其他请求只能干等。
别用单事务包整个WHILE循环
这是最常见、杀伤力最大的反模式:BEGIN TRANSACTION后套一个处理10万行的WHILE,最后才COMMIT。哪怕每行UPDATE只要几毫秒,10万行加锁时间也轻松突破分钟级。
- 每批控制在
5000行以内,UPDATE TOP (@batch_size)+COMMIT必须成对出现 -
WHERE条件字段(如status = 'pending')必须有索引,否则每次查都全表扫描,锁范围直接升级 - 禁用
OFFSET / FETCH分页更新,它越往后越慢;改用主键推进式分批,例如WHERE id > @last_id AND status = 'pending'
SELECT FOR UPDATE 别放太早
很多人一上来就 SELECT ... FOR UPDATE,然后做日志、校验、调外部服务,锁从第一行开始就捏着不放——后面所有操作都在等这把锁。
- 把
SELECT ... FOR UPDATE紧贴在真正要UPDATE的语句前,中间不穿插任何非数据库逻辑 - 如果只是防重复提交,优先用
INSERT IGNORE或ON DUPLICATE KEY UPDATE,不依赖显式锁 - 加
NOWAIT,让抢不到锁的请求立刻失败(报错 1205),而不是无限等待
事务内禁止执行耗时操作
事务一旦开始,所有加的锁都会等到 COMMIT 或 ROLLBACK 才释放。网络请求、文件读写、JSON解析这些操作,会让锁挂起几十秒甚至更久。
-
EXEC xp_cmdshell、sp_OACreate、HTTP 调用(如sp_http_get)必须移出事务 - 日志记录、消息推送、风控计算等业务逻辑,放到
COMMIT之后再做 - 时间格式化尽量用原生函数(如
FORMAT),避免游标或自定义 CLR 逐行处理
索引缺失会让行锁变表锁
没有索引的 WHERE 条件,MySQL/SQL Server 都可能放弃行级锁,直接升级为页锁甚至表锁——这时并发数再高也没用,大家全在排队。
- 用
EXPLAIN(MySQL)或执行计划(SQL Server)确认type是ref或const,不是ALL或index - 复合索引顺序要匹配查询条件,
WHERE status = ? AND user_id = ?应建(status, user_id),反过来无效 - 避免
WHERE DATE(created_at) = '2024-01-01'这类函数操作,会跳过索引
最容易被忽略的是:事务是否真的结束了。应用异常退出、连接池复用未清理、调试时手动中断,都会留下 trx_state = 'RUNNING' 但 trx_started 几分钟前的“幽灵事务”——它不争锁,但拖慢 MVCC 清理、阻塞 DDL,还占着连接资源。

















