MyBatis SQL耗时监控的最佳实践

 更新时间:2025年08月06日 09:01:10   作者:天天摸鱼的java工程师  
在性能优化的道路上,SQL执行时间往往是关键突破口,本文将从一个八年经验Java开发者的视角,带你深入探索MyBatis SQL耗时监控的最佳实践,需要的朋友可以参考下

一、为什么我们需要关注SQL执行时间?

在我多年的开发经历中,数据库性能瓶颈往往是系统优化的关键点。特别是在微服务架构和高并发场景下,一个未优化的SQL可能拖垮整个系统。以下是几个典型的业务场景:

  1. 生产环境性能问题排查:用户突然反馈系统变慢,需要快速定位慢SQL
  2. 接口性能优化:API响应时间超过预期,需要分析数据库操作耗时
  3. 慢SQL监控:建立系统化的慢查询监控机制
  4. SQL优化验证:验证SQL优化措施的实际效果

二、技术方案选型:为什么选择MyBatis拦截器?

在Java生态中,我们有多种方式可以实现SQL耗时统计:

方案优点缺点
MyBatis拦截器原生支持、侵入性低、精准控制需要理解MyBatis内部机制
AOP切面通用性强、与ORM解耦无法获取SQL语句细节
日志框架简单直接日志量大、难以结构化处理
数据库监控无需代码改动脱离应用上下文、配置复杂

作为有经验的开发者,我推荐使用MyBatis拦截器方案。它提供了最直接的SQL执行切入点,能获取最丰富的执行上下文信息,且对业务代码零侵入。

三、核心实现:从拦截器到耗时统计

3.1 实现原理

MyBatis拦截器基于责任链模式,我们可以通过实现Interceptor接口,在以下关键点插入逻辑:

  1. Executor.update() - 拦截增删改操作
  2. Executor.query() - 拦截查询操作

3.2 完整代码实现

import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Properties;

@Intercepts({
    @Signature(type = Executor.class, method = "update", 
               args = {MappedStatement.class, Object.class}),
    @Signature(type = Executor.class, method = "query",
               args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
    @Signature(type = Executor.class, method = "query",
               args = {MappedStatement.class, Object.class, RowBounds.class, 
                       ResultHandler.class, CacheKey.class, BoundSql.class})
})
public class SqlCostInterceptor implements Interceptor {
    
    private static final Logger logger = LoggerFactory.getLogger("SQL_PERF");
    
    // 慢查询阈值(毫秒)
    private long slowQueryThreshold = 1000;
    
    // 是否打印参数
    private boolean showParameters = true;
    
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        MappedStatement mappedStatement = (MappedStatement) args[0];
        Object parameterObject = args[1];
        
        // 获取SQL信息
        BoundSql boundSql = mappedStatement.getBoundSql(parameterObject);
        String sqlId = mappedStatement.getId();
        String sql = boundSql.getSql().replaceAll("\\s+", " ");
        
        long start = System.currentTimeMillis();
        try {
            return invocation.proceed(); // 执行目标方法
        } finally {
            long cost = System.currentTimeMillis() - start;
            logSqlPerformance(sqlId, sql, parameterObject, cost);
        }
    }
    
    private void logSqlPerformance(String sqlId, String sql, 
                                   Object params, long cost) {
        // 基础日志
        String logMsg = String.format("SQL执行耗时: %dms | ID: %s | SQL: %s",
                                     cost, sqlId, sql);
        
        // 参数日志(敏感数据需脱敏)
        if (showParameters && params != null) {
            String paramStr = params.toString();
            // 简单脱敏处理(实际项目应使用专业脱敏工具)
            paramStr = paramStr.replaceAll("(\"password\":\")([^\"]+)(\")", 
                                          "$1****$3");
            logMsg += " | 参数: " + paramStr;
        }
        
        // 分级日志输出
        if (cost > slowQueryThreshold) {
            logger.warn("[慢查询告警] {}", logMsg);
        } else if (cost > 500) {
            logger.info("{}", logMsg);
        } else {
            logger.debug("{}", logMsg);
        }
    }
    
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
    
    @Override
    public void setProperties(Properties properties) {
        // 从配置加载参数
        String threshold = properties.getProperty("slowQueryThreshold");
        if (threshold != null) {
            this.slowQueryThreshold = Long.parseLong(threshold);
        }
        
        String showParams = properties.getProperty("showParameters");
        if (showParams != null) {
            this.showParameters = Boolean.parseBoolean(showParams);
        }
    }
}

3.3 关键代码解析

  1. @Intercepts注解:定义拦截的类和方法签名
  2. BoundSql获取:通过MappedStatement获取实际执行的SQL
  3. 参数脱敏处理:防止敏感数据(如密码)泄露到日志
  4. 分级日志输出:根据耗时长短使用不同日志级别
  5. 可配置化:通过setProperties方法实现阈值可配置

3.4 Spring Boot集成配置

@Configuration
public class MyBatisConfig {
    
    @Bean
    public SqlCostInterceptor sqlCostInterceptor() {
        SqlCostInterceptor interceptor = new SqlCostInterceptor();
        Properties properties = new Properties();
        properties.setProperty("slowQueryThreshold", "500"); // 500ms以上视为慢查询
        properties.setProperty("showParameters", "true");
        interceptor.setProperties(properties);
        return interceptor;
    }
}

四、生产环境进阶技巧

4.1 性能优化策略

异步日志输出:避免日志IO阻塞业务线程

private final ExecutorService logExecutor = 
    Executors.newSingleThreadExecutor();

private void logSqlPerformance(...) {
    logExecutor.submit(() -> {
        // 日志记录逻辑
    });
}

采样率控制:高并发场景下按比例采样

// 在intercept方法中加入
if (ThreadLocalRandom.current().nextDouble() < 0.2) {
    // 只记录20%的请求
}

4.2 监控系统集成

将SQL耗时数据发送到监控系统(如Prometheus):

private void recordMetrics(String sqlId, long cost) {
    // Prometheus指标记录
    Summary.builder("sql_execution_time")
           .tag("sql_id", sqlId)
           .register()
           .observe(cost);
    
    // 慢查询计数器
    if (cost > slowQueryThreshold) {
        Counter.builder("slow_sql_count")
               .tag("sql_id", sqlId)
               .register()
               .inc();
    }
}

4.3 可视化与告警

结合Grafana等工具创建SQL性能仪表盘:

  1. 按SQL ID统计平均耗时
  2. 慢查询发生频率
  3. SQL执行次数统计

设置告警规则:

  • 单个SQL平均耗时连续5分钟 > 500ms
  • 慢查询频率每分钟 > 10次

五、经验总结与避坑指南

在八年开发实践中,我总结了以下重要经验:

谨慎处理参数日志

if (paramStr.length() > 1000) {
    paramStr = paramStr.substring(0, 1000) + "...[truncated]";
}
  • 必须进行敏感信息脱敏
  • 大对象参数截断处理

区分环境配置

  • 开发环境:记录详细SQL和参数
  • 生产环境:仅记录慢查询,关闭参数打印

线程安全考量

  • 拦截器是单例的,所有状态变量必须线程安全
  • 使用ThreadLocal存储耗时上下文

避免过度监控

  • 核心业务SQL重点监控
  • 低频管理后台SQL可降低监控强度

与其他监控工具协同

  • 整合链路追踪(如SkyWalking)的TraceID
  • 与APM工具协同工作

六、结论

记录MyBatis SQL执行耗时不是目的,而是优化系统性能的手段。通过本文介绍的拦截器方案,我们可以:

  1. 精准定位性能瓶颈
  2. 建立SQL性能基线
  3. 主动发现慢查询问题
  4. 量化SQL优化效果

技术不是目的,而是解决问题的工具。八年的经验告诉我:最好的优化发生在设计阶段,最有效的监控是预防性监控

以上就是MyBatis SQL耗时监控的最佳实践的详细内容,更多关于MyBatis SQL耗时监控的资料请关注脚本之家其它相关文章!

相关文章

  • SpringBoot项目在IntelliJ IDEA中如何实现热部署

    SpringBoot项目在IntelliJ IDEA中如何实现热部署

    spring-boot-devtools是一个为开发者服务的一个模块,其中最重要的功能就是自动应用代码更改到最新的App上面去。,这篇文章主要介绍了SpringBoot项目在IntelliJ IDEA中如何实现热部署,感兴趣的小伙伴们可以参考一下
    2018-07-07
  • Java实现把文件及文件夹压缩成zip

    Java实现把文件及文件夹压缩成zip

    这篇文章主要介绍了Java实现把文件及文件夹压缩成zip,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-03-03
  • Java Mybatis的初始化之Mapper.xml映射文件的详解

    Java Mybatis的初始化之Mapper.xml映射文件的详解

    这篇文章主要介绍了Java Mybatis的初始化之Mapper.xml映射文件的详解,解析完全局配置文件后接下来就是解析Mapper文件了,它是通过XMLMapperBuilder来进行解析的
    2022-08-08
  • 基于FeignClient调用超时的处理方案

    基于FeignClient调用超时的处理方案

    这篇文章主要介绍了基于FeignClient调用超时的处理方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-07-07
  • 强烈推荐这些提升代码效率的IDEA使用技巧

    强烈推荐这些提升代码效率的IDEA使用技巧

    在平常的开发中,发现一些同事对Idea 使用的不是很熟练,仅仅用来编辑,编译,不能很好的发挥Idea 的神奇.整理了下我平常用的一些技巧,希望你能从中学习到一些.需要的朋友可以参考下
    2021-05-05
  • springboot运行时新增/更新外部接口的实现方法

    springboot运行时新增/更新外部接口的实现方法

    这篇文章主要介绍了springboot运行时新增/更新外部接口的实现方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-03-03
  • springboot+websocket实现并发抢红包功能

    springboot+websocket实现并发抢红包功能

    本文主要介绍了springboot+websocket实现并发抢红包功能,主要包含了4种步骤,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-12-12
  • Java并发编程必备之Synchronized关键字深入解析

    Java并发编程必备之Synchronized关键字深入解析

    本文我们深入探索了Java中的Synchronized关键字,包括其互斥性和可重入性的特性,文章详细介绍了Synchronized的三种使用方式:修饰代码块、修饰普通方法和修饰静态方法,感兴趣的朋友一起看看吧
    2025-04-04
  • 深入浅析drools中Fact的equality modes

    深入浅析drools中Fact的equality modes

    这篇文章主要介绍了drools中Fact的equality modes的相关知识,本文通过图文实例代码相结合给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-05-05
  • SpringBoot配置文件中系统环境变量存在特殊字符的处理方式

    SpringBoot配置文件中系统环境变量存在特殊字符的处理方式

    这篇文章主要介绍了SpringBoot配置文件中系统环境变量存在特殊字符的处理方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-02-02

最新评论