MySQL存储过程高负载OOM主因是sp_head::main_mem_root内存泄漏,非物理内存不足;需通过performance_schema查memory/sql/sp_head::main_mem_root用量超1GB且持续上涨来确认,并强制配对CLOSE游标、加异常处理器、分页拉取等修复。

MySQL 存储过程在高负载下内存溢出,基本不是“内存不够”,而是 sp_head::main_mem_root 持续累积、不释放导致的 OOM Killer 杀进程。修复核心不在加内存,而在切断内存泄漏链路。
怎么确认是 sp_head::main_mem_root 吃光内存
别凭感觉猜,直接查 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_buffer_pool_size无关,是 SQL 层上下文独占内存 - 配合
SHOW PROCESSLIST看是否有连接长期卡在executing或Sending data,且对应 SQL 含DECLARE CURSOR FOR SELECT
CLOSE cursor_name 必须显式写,且异常路径也要覆盖
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同一个游标却不CLOSE——不能指望“下次覆盖”来清理
避免单次 FETCH 拉太多数据
游标本身不限大小,但每次 FETCH 都会把整行内容拷贝进 main_mem_root,且不复用。拉 10 万行,可能瞬间吃掉 500MB+。
- 业务允许的话,把遍历逻辑提到应用层(如 Python/Java),数据库只做分页查询,内存压力立刻转移
- 若必须在存储过程中处理,改用主键分段拉取:
SELECT id FROM t WHERE id > ? ORDER BY id LIMIT 1000,再用这批 ID 精准 JOIN - 游标查询里禁用
SELECT *,只选必要字段;避免多层CONCAT()拼接超长字符串,每行越精简,内存占用越低
临时止血:配置协同压制 + 快速释放
线上救急不能只等改代码,要立刻阻断内存增长并释放已占资源:
- 立即执行:
SET GLOBAL performance_schema = OFF—— 可快速释放已累积的performance_schema内存开销(重启后失效,但见效快) - 调小
table_open_cache_instances到 1:SET GLOBAL table_open_cache_instances = 1,防止每个实例缓存一份完整触发器副本 - 中断正在跑的长耗时过程:
KILL QUERY [connection_id](不是KILL),再检查SHOW PROCESSLIST是否还有残留 - 如果表上有大量触发器,临时禁用非关键触发器:
ALTER TABLE xxx DISABLE TRIGGER trigger_name
真正难的不是写 CLOSE,而是在嵌套块、异常分支、循环边界里都保证它被执行;最容易被忽略的是 table_open_cache_instances 和触发器加载机制——它们会让内存消耗翻倍,却完全不体现在你的存储过程代码里。


















