未设退出条件的while循环会导致mysql cpu飙升至100%、连接不释放、服务卡死;因其单线程纯计算执行,无io/锁等待,持续占用cpu时间片,常见于赋值失败、控制变量未更新或依赖未变化的外部状态;必须加计数器硬限兜底。

存储过程里没设退出条件的 WHILE 循环,大概率会把 MySQL CPU 打满到 100%,而且连接不释放、服务卡死——这不是小概率事件,是线上高频事故。
为什么死循环会让 CPU 直接飙到 100%
MySQL 的存储过程在单线程内执行,WHILE 循环体如果没 LEAVE、没 ITERATE、也没外部中断机制,就会在同一个会话里反复执行同一段逻辑。CPU 没 IO 等待、没锁等待,纯计算+判断+跳转,调度器持续分配时间片,结果就是该线程独占一个 CPU 核心。你看到 SHOW PROCESSLIST 里状态长期是 Executing 或 Updating,cpu_time 每秒涨几百毫秒,就是它在狂转。
常见诱因包括:
- 用
SELECT ... INTO赋值失败(比如查不到数据),变量保持初始值,导致循环条件始终为真 - 循环体内修改了控制变量,但赋值语句被
IF条件挡住,实际没执行 - 依赖外部表状态变化作为退出条件,而该表恰好被锁住或没更新
必须加循环计数器:用 max_iter 给 WHILE 设硬上限
别信“逻辑上一定会退出”,生产环境要按最坏情况兜底。在 WHILE 前声明计数器和最大允许迭代次数,每次循环末尾强制自增,并在开头检查是否超限。
示例写法:
DECLARE i INT DEFAULT 0; DECLARE max_iter INT DEFAULT 10000; my_loop: WHILE i <p>关键点:</p>
-
max_iter值不是拍脑袋定的:根据业务单次调用预期最大处理行数 × 1.5,再向上取整到千位(比如预估最多处理 3200 行,就设 5000) - 循环结束后必须验证是否真因条件满足退出:
SELECT i, some_condition;,如果i = max_iter还没LEAVE,说明逻辑有缺陷,得修 - 不要用
SLEEP(0.01)之类“降速”手段代替计数器——它只是让 CPU 占用从 100% 变成 99%,问题没解决
更稳妥的做法:配合连接级超时,双重保险
光靠存储过程内计数器还不够。万一过程本身被阻塞(比如等锁)、计数器没机会执行,或者有人删了计数逻辑,风险仍在。所以要在连接层加一道闸。
推荐做法:
- 调用存储过程前,临时设置会话级超时:
SET SESSION max_execution_time = 30000;(单位毫秒,即 30 秒) - 这个参数对
SELECT有效,对存储过程内的语句也生效(MySQL 5.7.8+) - 注意:
max_execution_time不影响INSERT/UPDATE/DELETE,所以如果过程里主要是 DML,还得靠计数器 - 上线前用
KILL QUERY [thread_id]测试下超时是否真能中断,避免参数被全局配置覆盖
运行后必须检查的三件事,缺一不可
哪怕你写了计数器、设了超时,存储过程跑完也不能只看 Query OK 就以为万事大吉。
- 立刻查
information_schema.processlist,确认该连接已断开或状态变为Sleep;如果还挂着且Time值持续增长,说明没真正退出 - 检查错误日志里有没有
Query execution was interrupted,这是max_execution_time触发的信号,意味着你的兜底起了作用 - 用
SHOW PROFILE FOR QUERY N(N 是过程执行的 query id)看耗时分布,重点看executing和end阶段是否异常长——这往往是循环体内部低效操作的征兆,比如没索引的子查询嵌套在循环里
最容易被忽略的是:计数器变量作用域混乱。比如在嵌套 BEGIN...END 块里重新 DECLARE 了同名变量,外层计数器根本没被更新。这种 bug 不报错,但会让计数器失效——务必用 SELECT 在关键位置输出变量值来验证。










