答案是sp_head::main_mem_root内存未释放所致;需查performance_schema中memory/sql/sp_head::main_mem_root用量超1GB即确认,且必须显式CLOSE游标、加异常处理、FETCH后立即检查done。

MySQL 存储过程里游标不 CLOSE,内存不会自动释放——这不是“可能泄漏”,而是确定性行为。哪怕过程执行完、连接还活着,sp_head::main_mem_root 分配的内存就一直挂着,多个并发调用后 RSS 内存线性上涨,轻则变慢,重则被 OOM Killer 杀掉。
怎么确认是游标导致的内存暴涨
别猜,直接查 performance_schema:
- 先确认监控已启用:
SELECT VARIABLE_VALUE FROM performance_schema.global_variables WHERE VARIABLE_NAME = 'performance_schema_instrument',返回值里必须含memory/% = COUNTED - 再查关键指标:
SELECT EVENT_NAME, CURRENT_NUMBER_OF_BYTES_USED FROM performance_schema.memory_summary_global_by_event_name WHERE EVENT_NAME = 'memory/sql/sp_head::main_mem_root',超过 1GB 基本就是它 - 配合
SHOW PROCESSLIST看有没有长期卡在executing或Sending data的连接,且 SQL 含DECLARE CURSOR FOR SELECT
为什么 CLOSE 必须显式写,且异常路径不能漏
MySQL 不会在存储过程退出时自动释放游标占用的 sp_head::main_mem_root 内存。哪怕你用 LEAVE 跳出循环,只要没执行 CLOSE cursor_name,这块内存就永远驻留。
-
DECLARE CURSOR后必须紧跟着配对的OPEN和CLOSE,且CLOSE必须写在END PROCEDURE之前 - 异常路径(如
SQLEXCEPTION)下CLOSE容易被跳过,必须用DECLARE CONTINUE HANDLER FOR SQLEXCEPTION BEGIN CLOSE cursor_name; LEAVE proc_exit; END; - 不要把
CLOSE放在循环体里,也不要依赖“最后统一写一句”——所有退出路径(正常结束、LEAVE、异常)都得覆盖到
FETCH 后不立刻检查 done 会触发空行误处理
MySQL 游标没有“预判是否还有下一行”的机制。FETCH 执行后,只有下一次 FETCH 触发 NOT FOUND 异常才会设置 done = TRUE。如果 FETCH 后没立刻判断 done,而是先做业务逻辑,就会对空行重复处理。
-
DECLARE done INT DEFAULT FALSE必须放在变量声明区,不能在循环中反复SET done = 0 -
FETCH后必须紧跟IF done THEN LEAVE read_loop;,中间不能插其他语句 - 嵌套游标需为每个游标单独声明
done变量和 handler,不能复用
最隐蔽的坑是:你以为加了 CLOSE 就万事大吉,但没包异常 handler、CLOSE 写在错误位置、或游标在嵌套块中声明却跨块 CLOSE,都会让那块内存彻底“失联”。上线前压测时,一定要开多连接跑,盯着 ps aux --sort=-%mem 里 mysqld 的 RSS 是否线性上涨——这才是真实压力下的泄漏信号。

















