MySQL存储过程无原生执行超时机制,max_execution_time对其无效;需依赖客户端query timeout(如JDBC setQueryTimeout)、过程内手动时间埋点、调整innodb_lock_wait_timeout和wait_timeout参数,并确保结果集消费与资源释放。

MySQL 存储过程执行超时不会直接“中断连接”,但会引发客户端主动 KILL,进而触发 You are not owner of thread 这类错误——根本原因是超时发生在客户端或中间件层,而非 MySQL 自身事务级控制。
客户端 query timeout 是第一道也是最有效的防线
MySQL 服务端没有“存储过程总执行时间限制”语法,max_execution_time 只对单条语句生效,对 CALL 本身无效。真正能拦住超时的,是调用方设置的 query timeout:
- Java JDBC:
PreparedStatement.setQueryTimeout(30),超时后驱动自动发KILL QUERY并抛SQLException - Python PyMySQL:
cursor.execute("CALL proc_xxx()", timeout=30),超时抛pymysql.OperationalError - Go database/sql:
db.QueryContext(ctx, "CALL proc_xxx()"),其中ctx由context.WithTimeout创建 - MyBatis XML 中可单独为存储过程配置:
<select id="xxx" statementType="CALLABLE" timeout="300">{call proc_xxx()}
这个 timeout 必须显式设,不能依赖连接池全局 wait_timeout——后者只在连接空闲时倒计时,一旦过程里执行了 SQL,计时就重置。
存储过程内部必须加时间兜底和事务闭环
客户端 timeout 是外部拦截,但过程内逻辑失控(比如卡在 SLEEP()、HTTP 回调、死循环)时,它无法介入。必须手动埋点:
- 开头记录起点:
SELECT NOW() INTO @start_time; - 关键节点检查:
IF TIMESTAMPDIFF(SECOND, @start_time, NOW()) > 25 THEN ROLLBACK; LEAVE proc_main; END IF;(留 5 秒余量给客户端收尾) - 所有
START TRANSACTION必须包裹在DECLARE EXIT HANDLER FOR SQLEXCEPTION里,否则异常跳出时事务悬空,连接被锁死 - 绝对避免在过程里调用
GET_LOCK()、SLEEP()或外部系统——这些操作不触发innodb_lock_wait_timeout,也不受max_execution_time约束
两个 session 级参数必须显式调整
别依赖全局值,每次调用前在存储过程开头或客户端连接初始化时设:
-
SET SESSION innodb_lock_wait_timeout = 15;:控制“等行锁最多忍多久”,默认 50 秒太长,15 秒能更快暴露争抢问题;注意它只对SELECT ... FOR UPDATE、UPDATE等加锁操作生效 -
SET SESSION wait_timeout = 300;:设为 5 分钟,防止过程卡在非 SQL 操作(如循环计算)时连接无限期挂着;interactive_timeout不生效,因为存储过程调用走的是非交互式连接
这两个参数不解决“执行慢”,但能防止连接被长期占用——而连接池耗尽,往往就始于一个没释放的“Sleep”连接。
别漏掉结果集消费和资源关闭
存储过程返回多个 SELECT 语句时,JDBC 要求客户端显式获取并关闭每一个 ResultSet,否则连接会卡在 “Waiting for next result set” 状态:
- 用
try-with-resources确保CallableStatement关闭:try (CallableStatement cs = conn.prepareCall("{CALL proc_xxx()}")) { cs.execute(); } - 若需读结果,必须循环:
do { try (ResultSet rs = cs.getResultSet()) { /* consume */ } } while (cs.getMoreResults()); - HikariCP 开启泄露检测:
leakDetectionThreshold=60000,60 秒未归还就打堆栈,精准定位哪行cs.execute()后面忘了关
最容易被忽略的其实是“过程跑完了,但连接没释放”——它不报错,只悄悄把连接池撑满,直到新请求全卡在 getConnection() 上。


















