性能监控插件
MyBatis 性能监控插件通过拦截 SQL 执行全过程,记录执行耗时、SQL 文本、参数等指标,为慢查询分析和性能优化提供数据支撑。
拦截时机选择
| 拦截对象 | 拦截方法 | 可获取信息 | 适用场景 |
|---|---|---|---|
Executor | query/update | MappedStatement、参数、RowBounds | 记录完整执行上下文,含映射语句信息 |
StatementHandler | query/update | 最终 SQL、参数、执行结果 | 最接近实际执行点,时间精度最高 |
ResultSetHandler | handleResultSets | 结果集大小、映射关系 | 分析结果集规模与映射性能 |
推荐拦截
StatementHandler的query和update方法,此时 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();
}
}
注意事项
- 性能开销:监控插件本身有性能损耗,生产环境建议抽样或仅开启慢查询监控
- SQL 脱敏:记录 SQL 时注意敏感数据脱敏,参数值可能包含隐私信息
- 内存控制:指标聚合器需限制存储容量,避免内存泄漏,定期清理过期数据
- 异步记录:耗时计算和日志输出应异步执行,避免阻塞主 SQL 执行线程
要点总结
- 拦截
StatementHandler的 query/update/batch 方法,在 SQL 执行前后记录时间差 - 慢查询通过阈值判定,可自动触发 EXPLAIN 分析辅助定位问题
- 可收集执行时间、次数、成功率、SQL 长度等多维度指标,支持 P99 等分位数统计
- 指标可对接 Prometheus/Micrometer 等外部监控系统,便于集中展示与告警
- 监控插件本身有性能开销,生产环境需控制采样率和内存占用,敏感数据需脱敏
存放路径:D:\git2\jwdev\articles\MYBATIS\进阶\插件机制\性能监控插件.md
📝 发现内容有误?点击此处直接编辑