mysql存储过程应避免游标循环更新,改用集合操作;显式控制事务边界及时提交;批量操作使用多值insert或join更新;热点行更新改用状态暂存表+异步聚合,接受最终一致性。

存储过程里别用游标循环更新
游标(DECLARE CURSOR)在 MySQL 8.0 中本质是单行迭代,无法并行,且在 RR 隔离级别下容易触发间隙锁,多个并发调用会互相阻塞。尤其当游标遍历条件涉及范围查询(如 WHERE status = 'pending'),InnoDB 可能对整个索引区间加锁,导致后续事务全被堵住。
- 改用集合操作:把
FETCH+UPDATE循环替换成UPDATE ... JOIN或INSERT INTO ... SELECT - 避免在游标体里做
SELECT FOR UPDATE—— 这会让锁持有时间不可控,且无法跳过已锁定行 - 如果必须逐行处理,先用
SELECT id FROM t WHERE ... LOCK IN SHARE MODE拿到 ID 列表,再分批UPDATE ... WHERE id IN (...),减少锁持有总时长
显式控制事务边界,别依赖自动提交
MySQL 存储过程默认在每个语句后自动提交(autocommit=1),但一旦你开头写了 BEGIN,就进入显式事务模式——而很多人忘了 COMMIT,结果整个存储过程执行完才释放锁,可能长达几秒甚至更久。
- 在存储过程开头明确写
START TRANSACTION,并在关键逻辑块后立即COMMIT,不要等过程结束 - 对只读逻辑(如预检查、计算),用
SET autocommit = 1临时切回自动提交,避免无谓持锁 - 监控
innodb_row_lock_time_avg和innodb_row_lock_current_waits,如果平均锁等待时间突增,大概率是某段逻辑没及时COMMIT
批量操作别拆成单行 INSERT/UPDATE
存储过程中用循环拼 INSERT INTO t VALUES (...) 或 UPDATE t SET x=y WHERE id=?,看似可控,实则放大锁争用:每条语句都触发一次解析、加锁、刷日志,网络往返和锁队列开销翻倍。
- 改用多值
INSERT:例如INSERT INTO t (a,b) VALUES (1,2),(3,4),(5,6),一批最多 500–1000 行 - 更新优先走
UPDATE t JOIN temp ON ... SET ...,把中间结果写进临时表再关联更新,避免重复扫描主表 - 禁用
INSERT ON DUPLICATE KEY UPDATE处理高频冲突场景——它会在唯一键上形成锁队列,比INSERT IGNORE+ 应用层重试更卡
热点行更新必须绕开直接写
如果存储过程反复更新同一行(比如计数器、用户余额),无论怎么优化 SQL,innodb_row_lock_waits 都会持续升高。这不是语法问题,而是访问模式缺陷——MySQL 的行锁机制没变,调参解决不了根本。
- 用状态暂存表:例如建
user_balance_delta(user_id, delta, processed),存储过程只做INSERT,不碰主表users - 异步聚合更新:另起定时任务或监听队列,批量执行
UPDATE users SET balance = balance + SUM(delta) FROM user_balance_delta ... GROUP BY user_id - 读取时合并:查余额时用
SELECT balance + COALESCE((SELECT SUM(delta) FROM user_balance_delta WHERE user_id = ? AND processed = 0), 0)
真正难的不是写出不锁的 SQL,而是说服业务方接受“最终一致性”——很多所谓“实时”需求,其实容忍几百毫秒延迟。这点最容易被忽略,也最影响落地效果。











