存储过程执行时内存不释放的典型表现是show processlist中大量sleep连接、threads_connected持续攀升、created_tmp_tables同步上涨而innodb_buffer_pool_pages_data无变化,说明问题在server层临时结构而非buffer pool。

存储过程执行时内存不释放的典型表现
MySQL 存储过程本身不“常驻内存”,但它的执行会触发一系列按连接分配、按查询生命周期申请的内存,且这些内存往往不会在存储过程结束时立即归还。常见现象包括:SHOW PROCESSLIST 中大量 Sleep 状态连接,Threads_connected 持续攀升,Created_tmp_tables 和 Created_tmp_disk_tables 计数同步上涨,而 innodb_buffer_pool_pages_data 却无明显变化——说明问题不在 Buffer Pool,而在 Server 层临时结构。
每个存储过程调用都在悄悄分配线程级缓冲区
MySQL 为每个连接线程预分配固定大小的私有缓冲区,而存储过程内嵌的 SQL 语句(尤其是含 ORDER BY、GROUP BY、多表 JOIN)会额外触发以下内存申请:
-
sort_buffer_size:每个排序操作独占一份,即使只有一条SELECT ... ORDER BY,也会按该值分配(默认 256KB,高并发下极易累积) -
join_buffer_size:未走索引的关联操作启用,按连接+每张非驱动表各一份 -
read_rnd_buffer_size:配合ORDER BY随机读取时使用 -
tmp_table_size/max_heap_table_size:存储过程中创建的临时表(如子查询结果集、CREATE TEMPORARY TABLE)首先进内存,超限才落磁盘
关键点:这些参数是“每连接”生效的。一个存储过程若循环调用 10 次复杂查询,就可能触发 10 倍缓冲区申请;若连接未关闭,这些内存不会回收。
存储过程内循环 + 临时表 = 内存缓慢爬升
业务中常见的低效写法是:在存储过程中用 WHILE 或 REPEAT 循环反复 INSERT INTO temp_table SELECT ...,或不断 CREATE TEMPORARY TABLE 后又 DROP。这会导致:
- 每次
CREATE TEMPORARY TABLE都需分配新内存块,旧块未必立即合并释放,产生碎片 - 频繁创建/销毁临时表会推高
Created_tmp_tables,若其值远高于Created_tmp_disk_tables,说明内存被大量占用但未溢出 - MySQL 5.7+ 的
temptable_max_mmap默认启用,底层用 mmap 分配,这部分内存不会反映在top的 RSS 中,但实际占用物理内存
验证方式:执行 SELECT event_name, current_number_of_bytes_used FROM performance_schema.memory_summary_global_by_event_name WHERE event_name LIKE 'memory/temptable/%' ORDER BY current_number_of_bytes_used DESC LIMIT 5;,看 memory/temptable/physical_ram 是否异常高。
为什么连接没关,存储过程却“卡住”了内存?
根本原因在于 MySQL 的连接生命周期管理与应用层脱节。存储过程执行完,只要连接还开着(比如应用用了连接池但没设置 idle_timeout),线程栈(thread_stack,默认 256KB–2MB)、上一次查询的排序/临时表内存、甚至解析器缓存都仍绑定在该连接上。更隐蔽的是:
- 存储过程中若开启事务但未
COMMIT或ROLLBACK,InnoDB 会保留 undo log 和锁结构,间接延长内存持有时间 - 某些 ORM 或中间件在调用存储过程后未显式
CLOSE游标(如DECLARE cur CURSOR FOR ...),导致游标资源长期挂起 -
wait_timeout默认 28800 秒(8 小时),Sleep 连接可存活太久,期间所有 per-connection 缓冲区持续占位
真正有效的缓解动作不是杀连接,而是从源头控制:把 wait_timeout 和 interactive_timeout 降至 300 秒以内,并确保应用层对存储过程调用有明确的连接归还逻辑——这点比调大 innodb_buffer_pool_size 关键得多。











