MySQL存储过程内存溢出主因是sp_head::main_mem_root长期驻留不释放,需通过performance_schema监控该值(>1GB即预警),强制显式CLOSE游标并覆盖异常路径,且并发调用会线性累积内存。

MySQL存储过程本身不直接报“内存溢出”,但实际运行中会持续推高 mysqld 进程的 RSS 内存,且重启前几乎不回落——根本原因在于 memory/sql/sp_head::main_mem_root 这块内存区域长期驻留、不随过程退出释放,尤其在游标未关闭、触发器密集或并发调用时,极易吃光物理内存。
为什么CLOSE游标不是可选项,而是强制动作
MySQL不会在存储过程执行结束时自动清理 sp_head::main_mem_root 中为游标分配的上下文内存。哪怕你用 LEAVE 跳出循环,只要没显式执行 CLOSE cursor_name,这块内存就一直挂着,直到连接断开。
- 每个
DECLARE CURSOR FOR SELECT ...都会在main_mem_root里注册一个执行上下文,FETCH 每一行都会把整行数据(含TEXT、大VARCHAR)缓存进去 - 嵌套块中声明的游标,
CLOSE必须写在同级BEGIN...END块内,跨块调用无效 - 异常路径必须覆盖:需配
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION,并在 handler 里先CLOSE再LEAVE,否则一报错就漏关
怎么确认真是游标在吃内存
别靠猜,直接查 performance_schema 里真正扛内存的模块:
- 执行
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(尤其远超
innodb_buffer_pool_size),基本锁定是它 - 再配合
SHOW PROCESSLIST看是否有线程长期卡在executing或Sending data,且对应 SQL 含DECLARE CURSOR -
dmesg | grep mysqld若出现Kill process mysqld (mysqld) score,而SHOW STATUS LIKE 'Innodb_buffer_pool_reads'并不高,说明不是 InnoDB 层问题,是 SQL 层内存失控
并发调用会让内存占用线性翻倍
每个客户端连接都独占一份 sp_head 实例,意味着同一段含游标的存储过程被 50 个连接同时执行,就会有 50 份 main_mem_root 并行累积——这不是 bug,是设计如此,但容易被忽略。
- 上线前压测必须模拟真实并发数,用脚本或
sysbench开多连接跑,观察ps aux --sort=-%mem | grep mysqld的 RSS 是否线性上涨 - 不要依赖“过程结束自动清理”——MySQL 5.7 和 8.0 都未实现该行为
- 若业务确实需要遍历大量数据,优先改用
UPDATE JOIN或INSERT SELECT等集合操作,绕过游标
容易被忽略的隐性放大点
除了游标,还有几个地方会让 main_mem_root 暗中膨胀:
- 表上存在大量触发器:每次打开表,所有关联触发器都会加载进该内存区;
table_open_cache_instances设得过大(如默认 16)会进一步放大 - 触发器里用了
SELECT ... INTO @var或拼接长字符串,变量内容也堆在main_mem_root里 -
max_binlog_cache_size完全不影响游标内存,它只管 binlog 事务缓存,别被名字误导 - 避免
SELECT *,尤其避开TEXT列;改用LIMIT 1000分批拉取,比单次 FETCH 10 万行安全得多
最危险的不是某次执行崩掉,而是内存缓慢爬升、三四天后突然被 OOM Killer 杀掉——这时候再查日志已经晚了。关键动作就三件:查 main_mem_root 实时用量、确保每个游标配对 CLOSE、压测时盯住并发连接下的 RSS 走势。其他优化都是锦上添花,这三步才是保命底线。


















