mysql存储过程内存溢出八成因游标未显式关闭,导致sp_head::main_mem_root持续占用内存;须查performance_schema中memory/sql/sp_head::main_mem_root用量,超1gb即确认,并确保每个declare cursor后配close及异常处理。

MySQL存储过程执行时内存溢出,八成是游标没管好——sp_head::main_mem_root在后台悄悄吃光RSS内存,而你还在查Innodb_buffer_pool_reads。
怎么看是不是游标导致的内存暴涨
别猜,直接查performance_schema里真正扛内存的模块:
- 运行
SELECT EVENT_NAME, CURRENT_NUMBER_OF_BYTES_USED FROM performance_schema.memory_summary_global_by_event_name WHERE EVENT_NAME LIKE 'memory/sql/sp_head%';,重点看memory/sql/sp_head::main_mem_root这一行。如果它占了1GB以上(尤其远超innodb_buffer_pool_size),基本就是它 -
SHOW PROCESSLIST里如果长期卡在executing或Sending data,且对应SQL含DECLARE ... CURSOR FOR SELECT,嫌疑进一步上升 -
dmesg报Kill process XXX (mysqld) score XXX但SHOW STATUS里缓冲池读取不高,说明不是InnoDB层问题,而是SQL层内存失控
游标必须显式CLOSE,且异常路径也要覆盖
MySQL不会在存储过程退出时自动释放main_mem_root,哪怕你用LEAVE跳出循环,只要没CLOSE游标,这块内存就一直挂着。
- 每个
DECLARE CURSOR后面,必须配对写CLOSE cursor_name;,放在END前 - 加
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION,里面强制CLOSE并LEAVE,否则一报错就漏关 - 不要依赖“过程结束自动清理”——这是MySQL 5.7/8.0都未实现的假定
- 如果游标在嵌套块中声明,
CLOSE也得在同级块里执行,跨块无效
别让单次FETCH拉太多数据
游标本身不限大小,但每次FETCH会把整行缓存进main_mem_root;一次拉10万行,就可能吃掉几百MB,还无法复用。
- 改写逻辑:用
LIMIT+OFFSET分批查,比如每次只SELECT ... LIMIT 1000,再用循环调用存储过程 - 避免
SELECT *,只取真正需要的字段,尤其避开TEXT、大VARCHAR列 - 如果必须遍历全表,优先考虑
UPDATE JOIN或INSERT ... SELECT这类集合操作,绕过游标 -
max_binlog_cache_size不影响游标内存,它只管binlog事务缓存,别被名字误导
并发调用时,每个连接独占一份sp_head
50个连接同时跑同一个含游标的存储过程,就会有50份main_mem_root并行累积——这不是泄漏,是设计如此,但容易被忽略。
- 上线前压测要模拟真实并发数,用
sysbench或脚本开多连接跑,观察ps aux --sort=-%mem里mysqld RSS是否线性上涨 - 临时缓解可设
SET GLOBAL max_connections = 20,但治标不治本;根本是缩短单次执行时间+减少游标使用频次 - 监控项要加一条:
performance_schema.memory_summary_by_thread_by_event_name,按线程看谁在狂吃main_mem_root - 如果业务允许,把游标逻辑提到应用层(如Java/Python)做流式处理,数据库只负责分页查询,内存压力立刻转移
最危险的不是游标本身,而是“它看起来工作正常”——没报错、没超时、只是RSS每天涨一点,直到某次高峰被OOM Killer一刀切。盯住memory/sql/sp_head::main_mem_root这个指标,比调任何sort_buffer_size都管用。











