关键是在JDBC执行上下文中识别慢SQL并异步告警,而非在Runnable中硬编码检测;推荐Spring AOP拦截DAO方法或DataSource代理增强,准确提取SQL、参数及上下文后异步发送。

在 Runnable 中记录慢 SQL 并告警,关键不是“启动新线程去执行 SQL”,而是在已有 JDBC 执行上下文中识别慢查询、提取 SQL 语句、附加上下文信息,并异步投递到告警中间件。Runnable 本身只是执行单元,真正拦截和判断发生在数据访问层(如 DataSource、JDBC Statement、MyBatis Interceptor 或 Spring AOP)。
用 Spring AOP 拦截 DAO 方法(推荐)
这是最可控、侵入性最小的方式。不依赖 Runnable 生命周期,而是聚焦于 SQL 执行行为本身:
- 定义切点匹配 Mapper 接口方法或 @Repository 类中的查询方法(如 execution(* com.example.dao..*Mapper.*(..)))
- 在环绕通知中记录开始时间,在 finally 块中计算耗时;若超阈值(如 500ms),则提取 SQL(可通过 MyBatis 的 BoundSql、或结合 SqlParameterSource + SqlSessionFactory 获取预编译 SQL)
- 构造告警事件对象(含 SQL、参数、耗时、调用栈、线程名、TraceID),交由线程池异步发送(避免阻塞业务线程)
基于 DataSource 的代理增强(通用性强)
适合无 Spring 环境或需全局控制的场景。包装原始 DataSource,重写 getConnection(),返回自定义 ConnectionWrapper:
- ConnectionWrapper 再包装 Statement/PreparedStatement,重写 executeQuery / executeUpdate 等方法
- 在 execute 前记录 startNano,执行后计算耗时;若超时,通过 getMetaData()、toString() 或反射获取实际执行的 SQL 字符串(注意 PreparedStatement 需替换 ? 占位符为实际参数值,可借助 p6spy 或自己实现简单参数填充逻辑)
- 告警事件统一走异步队列(如 Disruptor、BlockingQueue + 守护线程),避免日志打满或网络抖动影响主流程
与告警中间件对接的关键细节
发送环节必须解耦、容错、可降级:
立即学习“Java免费学习笔记(深入)”;
- 不要直接同步调用 HTTP/RPC 接口;使用内存队列 + 单独消费线程,失败时本地落盘(如 RocksDB 或文件)并定时重试
- 告警内容至少包含:SQL 片段(截断前 200 字)、参数摘要(如 {"uid":123,"status":"A"})、执行耗时(ms)、数据库实例名、应用名、主机 IP、TraceID(来自 MDC 或 SkyWalking 上下文)
- 设置频率限制(如每分钟同一条 SQL 最多告警 3 次),避免雪崩式通知;支持按 SQL 模板聚合(如把 WHERE id = ? 归为一类)
为什么不该在 Runnable.run() 里硬编码慢 SQL 检测
Runnable 是执行逻辑的载体,不是 SQL 的来源。你在 Runnable 里写的代码大概率是业务组装逻辑,真正的 SQL 执行在它调用的 service → mapper 层。强行在 run() 开头记时间、结尾算耗时,既无法拿到 SQL 字符串,也无法区分是 JDBC 耗时还是纯计算耗时。容易误报、漏报,且难以维护。
不复杂但容易忽略:慢 SQL 告警的价值不在“发出去”,而在“能定位、可复现、不干扰”。重点始终是准确捕获 SQL + 参数 + 上下文,再轻量异步外发。


















