存储过程并发瓶颈主因是事务未及时提交,导致锁长期持有;应小批量更新后立即提交、延迟SELECT FOR UPDATE、补全索引、启用READ_COMMITTED_SNAPSHOT并避免ORM隐式长事务。

核心结论:存储过程并发瓶颈,90%以上出在事务没及时收口,而不是SQL写得不够快。
为什么UPDATE后不马上COMMIT会让并发掉一半
事务从BEGIN到COMMIT之间所有加的锁(行锁、间隙锁、意向锁)都不会释放。哪怕你只改了一行,只要事务还开着,其他想读/写这行的请求就得排队等——不是等UPDATE执行完,是等整个事务结束。
- 常见错误现象:
INFORMATION_SCHEMA.INNODB_TRX里看到trx_state = "RUNNING"但trx_started时间远早于当前时间;SHOW ENGINE INNODB STATUS里出现大量lock wait timeout exceeded - 典型反模式:在存储过程开头
BEGIN TRANSACTION,中间调用HTTP请求或文件处理,最后才COMMIT——网络延迟直接变成锁等待时间 - MySQL 5.7+ 支持
UPDATE ... LIMIT 1000,拆成小批量更新后立即COMMIT,比单次全量UPDATE快且锁更轻
SELECT FOR UPDATE放太前,等于主动给后续操作设卡
如果业务逻辑需要“先查再改”,SELECT ... FOR UPDATE确实必要,但它会立刻加锁,并一直持有到事务结束。很多人把它放在存储过程最开头,结果后面所有操作都卡在这把锁上。
- 正确做法:把
SELECT FOR UPDATE尽量往后挪,紧贴真正要UPDATE的语句之前;中间别穿插日志记录、参数校验等无关操作 - 替代方案:如果只是防重复提交,用
INSERT IGNORE或ON DUPLICATE KEY UPDATE比SELECT+UPDATE组合更轻量,不依赖显式锁 - 注意:
SELECT FOR UPDATE在READ COMMITTED下只锁命中行,但在REPEATABLE READ下可能锁住间隙,范围更大
索引缺失时,UPDATE锁的是整张表,不是某几行
没有索引的WHERE条件会让MySQL放弃行锁,直接升级为表级锁。这时并发数再高也没用,大家全在排队等同一把表锁。
- 用
EXPLAIN确认执行计划中type字段是const或ref,不是ALL或index - 复合索引要注意顺序:
WHERE status = 'pending' AND user_id = 123,索引应建为(status, user_id),反过来无效 - 避免在
WHERE里对字段做函数操作,比如WHERE DATE(created_at) = '2024-01-01'会跳过索引
SQL Server里别只调SET TRANSACTION ISOLATION LEVEL就以为万事大吉
READ COMMITTED是默认级别,但它仍会对读取的数据加S锁,直到语句结束才释放——不是事务结束。两个并发存储过程同时读同一行,再各自去UPDATE,就会互相等对方释放S锁,死锁预警。
- 真正有效的解法是启用
READ_COMMITTED_SNAPSHOT:它让读操作不加S锁,而是从tempdb读版本快照 - 启用命令:
ALTER DATABASE [YourDB] SET READ_COMMITTED_SNAPSHOT ON,注意已存在的连接不受影响 - 但光开SNAPSHOT不够:如果UPDATE本身还在全表扫描,
tempdb版本链会疯狂膨胀,IO压力反而更大——必须同步补索引
最容易被忽略的一点:ORM框架(比如Django的@transaction.atomic或Spring的@Transactional)可能在你完全没意识到的情况下,把整个HTTP请求生命周期包进一个事务里。锁不是你写的SQL决定的,而是你交出去的那个连接决定的。

















