MySQL函数本身不报内存溢出,但sp_head::main_mem_root持续累积会触发OOM Killer;需通过performance_schema定位内存项,显式CLOSE游标并覆盖异常路径,避免跨块操作和大批次FETCH。

MySQL函数本身不会直接报“内存溢出”,但调用它的上下文(比如存储过程、触发器或客户端批量查询)会因 sp_head::main_mem_root 持续累积而触发 OOM Killer 杀掉 mysqld。修复重点不在函数代码里加 try-catch,而在切断内存累积链路。
查实是不是函数/游标在吃内存
别猜,先看 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,且随函数调用频率上升,基本锁定问题源——不是 InnoDB 缓冲池,是 SQL 层的函数/游标上下文没释放
函数内游标必须显式 CLOSE 且覆盖异常路径
MySQL 不会在函数退出时自动释放 main_mem_root,哪怕用了 LEAVE 或自然结束。漏关一个游标,每次调用就多挂一份内存:
- 每个
DECLARE cursor_name CURSOR FOR SELECT ...后,必须配对写CLOSE cursor_name,放在函数末尾END前 - 必须加异常处理器:
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION BEGIN CLOSE cursor_name; LEAVE proc_label; END;,否则一报错就永久泄漏 - 若游标在嵌套
BEGIN...END块中声明,CLOSE必须在同一块内执行,跨块无效 - 避免在循环里反复
OPEN cursor而不CLOSE;一次OPEN对应一次CLOSE,不能靠“下次覆盖”
减少单次 FETCH 的数据体积
游标本身不限大小,但每次 FETCH 会把整行内容拷贝进 main_mem_root,且不复用。拉 10 万行?可能瞬间吃掉 500MB+:
- 改用应用层流式处理:函数只做简单计算或单行转换,把分页逻辑提到 Java/Python,用
setFetchSize(Integer.MIN_VALUE)边拉边算 - 若必须在函数内遍历,拆成小批次:用
LIMIT 1000+ 自增偏移模拟分批,而不是 DECLARE 大游标 - 避免在游标查询里 SELECT * 或拼接超长字符串(如
CONCAT(...)套多层),字段越精简,每行内存占用越低
临时降级与配置协同压制
线上救急不能只等改代码,要立刻止血并防复发:
- 立即执行:
SET GLOBAL performance_schema = OFF,可快速释放已累积的 performance_schema 内存(重启失效,但见效快) - 调低
table_open_cache_instances到 1:它会让每个实例缓存一份完整的sp_head,默认 16 就等于放大 16 倍内存占用 - 检查并禁用非必要触发器:
SELECT TRIGGER_NAME FROM information_schema.TRIGGERS WHERE EVENT_OBJECT_TABLE IN ('t1','t2'),注释掉测试,很多“函数内存溢出”其实是触发器链带崩的 - 函数调用频率高时,考虑用
DETERMINISTIC+READS SQL DATA显式声明,让优化器更早识别可缓存性,减少重复解析开销
最易被忽略的是:函数里的游标 CLOSE 不是“写了就行”,它必须和 DECLARE 在同一作用域,且异常分支必须 100% 覆盖——少一个 CONTINUE HANDLER,就等于埋了一颗内存泄漏定时炸弹。


















