全部学科
NodeJS全栈
nodejs
Python全栈
python
小程序首页
📅 2026-05-20 6 分钟 ✍️ juanwangdev

性能监控插件

MyBatis 性能监控插件通过拦截 SQL 执行全过程,记录执行耗时、SQL 文本、参数等指标,为慢查询分析和性能优化提供数据支撑。

拦截时机选择

拦截对象拦截方法可获取信息适用场景
Executorquery/updateMappedStatement、参数、RowBounds记录完整执行上下文,含映射语句信息
StatementHandlerquery/update最终 SQL、参数、执行结果最接近实际执行点,时间精度最高
ResultSetHandlerhandleResultSets结果集大小、映射关系分析结果集规模与映射性能

推荐拦截 StatementHandlerqueryupdate 方法,此时 SQL 已最终生成,执行时间测量最准确。

核心实现

基础监控拦截器

Java
@Intercepts({
    @Signature(type = StatementHandler.class, method = "query", 
        args = {Statement.class, ResultHandler.class}),
    @Signature(type = StatementHandler.class, method = "update", 
        args = {Statement.class}),
    @Signature(type = StatementHandler.class, method = "batch", 
        args = {Statement.class})
})
public class PerformanceInterceptor implements Interceptor {
    
    // 慢查询阈值(毫秒)
    private long slowSqlThreshold = 1000;
    
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler handler = (StatementHandler) invocation.getTarget();
        BoundSql boundSql = handler.getBoundSql();
        String sql = boundSql.getSql();
        MappedStatement ms = null;
        
        // 通过反射获取 MappedStatement
        MetaObject metaObject = SystemMetaObject.forObject(handler);
        if (metaObject.hasGetter("delegate")) {
            ms = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        }
        
        String statementId = ms != null ? ms.getId() : "unknown";
        
        // 记录执行前时间
        long startTime = System.currentTimeMillis();
        
        try {
            // 执行原始 SQL
            Object result = invocation.proceed();
            
            // 计算耗时
            long elapsed = System.currentTimeMillis() - startTime;
            
            // 记录指标
            recordMetric(statementId, sql, elapsed, "SUCCESS");
            
            // 慢查询告警
            if (elapsed > slowSqlThreshold) {
                logSlowQuery(statementId, sql, elapsed);
            }
            
            return result;
        } catch (Throwable e) {
            long elapsed = System.currentTimeMillis() - startTime;
            recordMetric(statementId, sql, elapsed, "FAILED");
            throw e;
        }
    }
    
    private void recordMetric(String statementId, String sql, long elapsed, String status) {
        // 可对接 Prometheus、Micrometer 等指标系统
        System.out.printf("[%s] %s | %d ms | %s%n", status, statementId, elapsed, 
            sql.length() > 100 ? sql.substring(0, 100) + "..." : sql);
    }
    
    private void logSlowQuery(String statementId, String sql, long elapsed) {
        System.err.printf("[SLOW SQL] %d ms | %s | %s%n", elapsed, statementId, sql);
    }
    
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
    
    @Override
    public void setProperties(Properties props) {
        String threshold = props.getProperty("slowSqlThreshold", "1000");
        this.slowSqlThreshold = Long.parseLong(threshold);
    }
}

慢查询分析

慢查询特征

特征识别方式优化建议
执行时间长超过阈值(如 1s)检查执行计划、索引覆盖
扫描行数多EXPLAIN 分析 rows 列添加/优化索引
临时表/文件排序EXPLAIN 中 Using temporary/filesort优化查询结构、增加索引
全表扫描EXPLAIN 中 type=ALL添加 WHERE 条件索引

自动 EXPLAIN 分析

Java
private void analyzeSlowQuery(String originalSql, Connection connection) {
    String explainSql = "EXPLAIN " + originalSql;
    try (PreparedStatement ps = connection.prepareStatement(explainSql);
         ResultSet rs = ps.executeQuery()) {
        
        ResultSetMetaData metaData = rs.getMetaData();
        int columnCount = metaData.getColumnCount();
        
        // 打印 EXPLAIN 结果
        while (rs.next()) {
            for (int i = 1; i <= columnCount; i++) {
                System.out.printf("%s: %s%n", metaData.getColumnName(i), rs.getObject(i));
            }
            System.out.println("---");
        }
    } catch (SQLException e) {
        System.err.println("EXPLAIN analysis failed: " + e.getMessage());
    }
}

性能指标收集

可收集指标

指标类型具体指标用途
执行时间平均/最大/P99 耗时识别性能瓶颈
执行次数各 Statement 调用频次发现热点查询
成功率成功/失败比例监控异常趋势
SQL 长度SQL 文本字符数识别超大 SQL
参数数量绑定参数个数分析参数化复杂度
结果集大小返回记录数(查询)评估数据传输量

指标聚合器

Java
public class MetricsCollector {
    
    private final ConcurrentHashMap<String, StatementMetrics> metricsMap = new ConcurrentHashMap<>();
    
    public void record(String statementId, long elapsed, boolean success) {
        metricsMap.compute(statementId, (key, existing) -> {
            if (existing == null) {
                return new StatementMetrics(elapsed, success);
            }
            existing.update(elapsed, success);
            return existing;
        });
    }
    
    public List<StatementMetrics> getTopSlowStatements(int topN) {
        return metricsMap.values().stream()
            .sorted(Comparator.comparingLong(StatementMetrics::getMaxTime).reversed())
            .limit(topN)
            .collect(Collectors.toList());
    }
    
    public void printReport() {
        System.out.println("=== MyBatis Performance Report ===");
        getTopSlowStatements(10).forEach(m -> {
            System.out.printf("%s | count=%d | avg=%dms | max=%dms | p99=%dms | fail=%d%n",
                m.getStatementId(), m.getCount(), m.getAvgTime(),
                m.getMaxTime(), m.getP99Time(), m.getFailCount());
        });
    }
    
    static class StatementMetrics {
        private final String statementId;
        private long count;
        private long totalTime;
        private long maxTime;
        private long failCount;
        private final List<Long> times = new ArrayList<>();
        
        StatementMetrics(long elapsed, boolean success) {
            this.statementId = "";
            update(elapsed, success);
        }
        
        void update(long elapsed, boolean success) {
            this.count++;
            this.totalTime += elapsed;
            this.maxTime = Math.max(this.maxTime, elapsed);
            if (success) {
                this.times.add(elapsed);
            } else {
                this.failCount++;
            }
        }
        
        String getStatementId() { return statementId; }
        long getCount() { return count; }
        long getAvgTime() { return count > 0 ? totalTime / count : 0; }
        long getMaxTime() { return maxTime; }
        long getFailCount() { return failCount; }
        long getP99Time() {
            if (times.isEmpty()) return 0;
            Collections.sort(times);
            int index = (int) (times.size() * 0.99);
            return times.get(Math.min(index, times.size() - 1));
        }
    }
}

配置与集成

XML
<!-- mybatis-config.xml -->
<plugins>
    <plugin interceptor="com.example.PerformanceInterceptor">
        <property name="slowSqlThreshold" value="500"/>
    </plugin>
</plugins>

对接外部监控系统

Java
// 对接 Micrometer
private final MeterRegistry meterRegistry;

private void recordMetric(String statementId, String sql, long elapsed, String status) {
    Timer.builder("mybatis.query.duration")
        .tag("statement", statementId)
        .tag("status", status)
        .register(meterRegistry)
        .record(elapsed, TimeUnit.MILLISECONDS);
    
    if (elapsed > slowSqlThreshold) {
        Counter.builder("mybatis.slow.query")
            .tag("statement", statementId)
            .register(meterRegistry)
            .increment();
    }
}

注意事项

  1. 性能开销:监控插件本身有性能损耗,生产环境建议抽样或仅开启慢查询监控
  2. SQL 脱敏:记录 SQL 时注意敏感数据脱敏,参数值可能包含隐私信息
  3. 内存控制:指标聚合器需限制存储容量,避免内存泄漏,定期清理过期数据
  4. 异步记录:耗时计算和日志输出应异步执行,避免阻塞主 SQL 执行线程

要点总结

  • 拦截 StatementHandler 的 query/update/batch 方法,在 SQL 执行前后记录时间差
  • 慢查询通过阈值判定,可自动触发 EXPLAIN 分析辅助定位问题
  • 可收集执行时间、次数、成功率、SQL 长度等多维度指标,支持 P99 等分位数统计
  • 指标可对接 Prometheus/Micrometer 等外部监控系统,便于集中展示与告警
  • 监控插件本身有性能开销,生产环境需控制采样率和内存占用,敏感数据需脱敏

存放路径:D:\git2\jwdev\articles\MYBATIS\进阶\插件机制\性能监控插件.md

📝 发现内容有误?点击此处直接编辑

← 上一篇 分页插件原理
下一篇 → 插件拦截原理
想查看更多题目和详细解析?
小程序提供完整的题库、模拟考试和详细解析
马上就来

长按或扫描二维码,立即体验

扫码体验小程序
马上就来
使用微信扫描二维码
立即体验完整题库