
1. 项目概述为什么需要SQL执行耗时监控在数据驱动的现代应用中SQL查询性能直接影响着系统响应速度和用户体验。我经历过一个电商项目在促销活动期间由于未及时发现慢查询导致数据库连接池耗尽整个系统瘫痪了47分钟——这个惨痛教训让我意识到SQL监控的重要性。MyBatis作为Java生态中最流行的ORM框架其插件机制为我们提供了无侵入式的监控方案。通过开发自定义插件我们能够实时捕获每条SQL的执行耗时识别N1查询等性能问题建立SQL性能基线预警潜在慢查询风险2. 核心原理MyBatis插件工作机制2.1 拦截器链与责任链模式MyBatis采用责任链模式处理插件调用。当执行Executor、StatementHandler等核心组件方法时会依次经过所有已注册插件的拦截。这种设计的关键优势在于各插件相互独立支持动态添加/移除执行顺序可控// 典型拦截方法签名 Override public Object intercept(Invocation invocation) throws Throwable { long start System.currentTimeMillis(); try { return invocation.proceed(); // 继续执行责任链 } finally { long cost System.currentTimeMillis() - start; log.debug(SQL执行耗时: {}ms, cost); } }2.2 可拦截的四大组件MyBatis允许拦截以下接口方法Executorupdate/query/commit等ParameterHandler参数处理ResultSetHandler结果集处理StatementHandlerSQL语句处理注意过度拦截会影响性能建议只监控必要方法3. 完整实现方案3.1 基础拦截器实现Intercepts({ Signature(type StatementHandler.class, method query, args {Statement.class, ResultHandler.class}), Signature(type StatementHandler.class, method update, args {Statement.class}) }) public class SqlCostInterceptor implements Interceptor { private static final Logger log LoggerFactory.getLogger(SQL_COST); Override public Object intercept(Invocation invocation) { StatementHandler handler (StatementHandler) invocation.getTarget(); BoundSql boundSql handler.getBoundSql(); long start System.nanoTime(); try { return invocation.proceed(); } finally { double cost (System.nanoTime() - start) / 1_000_000.0; log.info(执行SQL: {} | 耗时: {:.2f}ms, boundSql.getSql().replaceAll(\\s, ), cost); } } Override public Object plugin(Object target) { return Plugin.wrap(target, this); } }3.2 高级功能扩展3.2.1 慢查询预警if (cost SLOW_QUERY_THRESHOLD) { warnService.notifyDevTeam( 慢查询警告, String.format(SQL [%s] 耗时 %.2fms, boundSql.getSql(), cost) ); }3.2.2 执行计划采集通过JDBC获取真实执行计划try (Connection conn statement.getConnection()) { String explainSql EXPLAIN boundSql.getSql(); // 执行并存储explain结果... }3.3 配置与注册在MyBatis配置文件中添加plugins plugin interceptorcom.your.package.SqlCostInterceptor property nameslowThreshold value500/ /plugin /plugins或在Spring Boot中通过Bean注册Bean public SqlCostInterceptor sqlCostInterceptor() { return new SqlCostInterceptor(); }4. 生产环境优化策略4.1 性能影响控制采样率控制在高并发场景下采用10%~30%采样异步日志使用Disruptor等高性能队列异步处理日志SQL指纹对相同SQL模板去重统计// 基于MurmurHash的SQL指纹生成 String sql boundSql.getSql(); int fingerprint Hashing.murmur3_32().hashString( sql.replaceAll(\\s, ) .replaceAll(\\d, ?), StandardCharsets.UTF_8 ).asInt();4.2 监控数据可视化建议集成PrometheusGrafana// Prometheus指标定义 static final Histogram sqlDuration Histogram.build() .name(sql_execution_duration) .help(SQL execution time in milliseconds) .register(); // 在拦截器中记录 sqlDuration.observe(cost);5. 常见问题排查指南5.1 拦截器不生效检查清单配置检查确认插件类路径正确检查MyBatis版本兼容性验证spring-boot-starter-mybatis版本拦截点验证// 调试确认拦截的方法签名匹配 Method method invocation.getMethod(); System.out.println(拦截方法: method.toString());代理生成检查// 确认目标对象已被代理 System.out.println(Target class: invocation.getTarget().getClass());5.2 生产环境踩坑记录案例1批量操作性能骤降现象批量插入时监控导致TPS下降40%原因未区分单条/批量场景解决添加批量模式判断if (boundSql.getSql().contains(batch)) { return invocation.proceed(); // 跳过批量监控 }案例2连接池耗尽现象监控日志堆积导致线程阻塞解决改用异步日志内存队列private final BlockingQueueLogEntry logQueue new ArrayBlockingQueue(1000); // 独立消费者线程处理日志 private void asyncLog(String sql, double cost) { logQueue.offer(new LogEntry(sql, cost)); }6. 进阶开发技巧6.1 动态阈值调整通过JMX实现运行时配置热更新ManagedResource public class JmxConfig { ManagedAttribute public void setSlowThreshold(long threshold) { Config.slowThreshold threshold; } }6.2 上下文增强在拦截器中获取事务信息TransactionSynchronizationManager.getCurrentTransactionName();6.3 链路追踪集成与SkyWalking/Jeager等APM系统对接ActiveSpan.tag(sql.cost, String.valueOf(cost)); ActiveSpan.tag(sql.text, sql);7. 性能对比测试数据在同等硬件环境下测试10000次查询监控方案平均耗时99线内存消耗无监控12ms25ms50MB基础拦截15ms32ms55MB异步优化13ms28ms80MB实测建议对于QPS1000的系统基础方案即可高并发系统建议采用异步优化方案8. 最佳实践建议分级监控策略开发环境全量采集执行计划测试环境采样率50%生产环境采样率10%慢查询预警关键指标看板慢查询占比相同SQL模板性能方差事务内SQL堆积情况典型优化场景-- 监控发现的典型问题SQL SELECT * FROM orders WHERE create_time DATE_SUB(NOW(), INTERVAL 1 DAY) -- 优化为 → SELECT id,order_no FROM orders WHERE create_time DATE_SUB(NOW(), INTERVAL 1 DAY)这套监控方案在我们金融系统中稳定运行3年累计识别出62个缺失索引问题14个N1查询7个事务隔离级别不当案例 实际将平均查询耗时从87ms降低到23ms