MyBatis SQL耗時監(jiān)控的最佳實踐
一、為什么我們需要關注SQL執(zhí)行時間?
在我多年的開發(fā)經(jīng)歷中,數(shù)據(jù)庫性能瓶頸往往是系統(tǒng)優(yōu)化的關鍵點。特別是在微服務架構和高并發(fā)場景下,一個未優(yōu)化的SQL可能拖垮整個系統(tǒng)。以下是幾個典型的業(yè)務場景:
- 生產(chǎn)環(huán)境性能問題排查:用戶突然反饋系統(tǒng)變慢,需要快速定位慢SQL
- 接口性能優(yōu)化:API響應時間超過預期,需要分析數(shù)據(jù)庫操作耗時
- 慢SQL監(jiān)控:建立系統(tǒng)化的慢查詢監(jiān)控機制
- SQL優(yōu)化驗證:驗證SQL優(yōu)化措施的實際效果
二、技術方案選型:為什么選擇MyBatis攔截器?
在Java生態(tài)中,我們有多種方式可以實現(xiàn)SQL耗時統(tǒng)計:
| 方案 | 優(yōu)點 | 缺點 |
|---|---|---|
| MyBatis攔截器 | 原生支持、侵入性低、精準控制 | 需要理解MyBatis內(nèi)部機制 |
| AOP切面 | 通用性強、與ORM解耦 | 無法獲取SQL語句細節(jié) |
| 日志框架 | 簡單直接 | 日志量大、難以結構化處理 |
| 數(shù)據(jù)庫監(jiān)控 | 無需代碼改動 | 脫離應用上下文、配置復雜 |
作為有經(jīng)驗的開發(fā)者,我推薦使用MyBatis攔截器方案。它提供了最直接的SQL執(zhí)行切入點,能獲取最豐富的執(zhí)行上下文信息,且對業(yè)務代碼零侵入。
三、核心實現(xiàn):從攔截器到耗時統(tǒng)計
3.1 實現(xiàn)原理
MyBatis攔截器基于責任鏈模式,我們可以通過實現(xiàn)Interceptor接口,在以下關鍵點插入邏輯:
Executor.update()- 攔截增刪改操作Executor.query()- 攔截查詢操作
3.2 完整代碼實現(xiàn)
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;
// 是否打印參數(shù)
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(); // 執(zhí)行目標方法
} 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執(zhí)行耗時: %dms | ID: %s | SQL: %s",
cost, sqlId, sql);
// 參數(shù)日志(敏感數(shù)據(jù)需脫敏)
if (showParameters && params != null) {
String paramStr = params.toString();
// 簡單脫敏處理(實際項目應使用專業(yè)脫敏工具)
paramStr = paramStr.replaceAll("(\"password\":\")([^\"]+)(\")",
"$1****$3");
logMsg += " | 參數(shù): " + 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) {
// 從配置加載參數(shù)
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 關鍵代碼解析
- @Intercepts注解:定義攔截的類和方法簽名
- BoundSql獲取:通過MappedStatement獲取實際執(zhí)行的SQL
- 參數(shù)脫敏處理:防止敏感數(shù)據(jù)(如密碼)泄露到日志
- 分級日志輸出:根據(jù)耗時長短使用不同日志級別
- 可配置化:通過setProperties方法實現(xiàn)閾值可配置
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;
}
}
四、生產(chǎn)環(huán)境進階技巧
4.1 性能優(yōu)化策略
異步日志輸出:避免日志IO阻塞業(yè)務線程
private final ExecutorService logExecutor =
Executors.newSingleThreadExecutor();
private void logSqlPerformance(...) {
logExecutor.submit(() -> {
// 日志記錄邏輯
});
}
采樣率控制:高并發(fā)場景下按比例采樣
// 在intercept方法中加入
if (ThreadLocalRandom.current().nextDouble() < 0.2) {
// 只記錄20%的請求
}
4.2 監(jiān)控系統(tǒng)集成
將SQL耗時數(shù)據(jù)發(fā)送到監(jiān)控系統(tǒng)(如Prometheus):
private void recordMetrics(String sqlId, long cost) {
// Prometheus指標記錄
Summary.builder("sql_execution_time")
.tag("sql_id", sqlId)
.register()
.observe(cost);
// 慢查詢計數(shù)器
if (cost > slowQueryThreshold) {
Counter.builder("slow_sql_count")
.tag("sql_id", sqlId)
.register()
.inc();
}
}
4.3 可視化與告警
結合Grafana等工具創(chuàng)建SQL性能儀表盤:
- 按SQL ID統(tǒng)計平均耗時
- 慢查詢發(fā)生頻率
- SQL執(zhí)行次數(shù)統(tǒng)計
設置告警規(guī)則:
- 單個SQL平均耗時連續(xù)5分鐘 > 500ms
- 慢查詢頻率每分鐘 > 10次
五、經(jīng)驗總結與避坑指南
在八年開發(fā)實踐中,我總結了以下重要經(jīng)驗:
謹慎處理參數(shù)日志:
if (paramStr.length() > 1000) {
paramStr = paramStr.substring(0, 1000) + "...[truncated]";
}
- 必須進行敏感信息脫敏
- 大對象參數(shù)截斷處理
區(qū)分環(huán)境配置:
- 開發(fā)環(huán)境:記錄詳細SQL和參數(shù)
- 生產(chǎn)環(huán)境:僅記錄慢查詢,關閉參數(shù)打印
線程安全考量:
- 攔截器是單例的,所有狀態(tài)變量必須線程安全
- 使用ThreadLocal存儲耗時上下文
避免過度監(jiān)控:
- 核心業(yè)務SQL重點監(jiān)控
- 低頻管理后臺SQL可降低監(jiān)控強度
與其他監(jiān)控工具協(xié)同:
- 整合鏈路追蹤(如SkyWalking)的TraceID
- 與APM工具協(xié)同工作
六、結論
記錄MyBatis SQL執(zhí)行耗時不是目的,而是優(yōu)化系統(tǒng)性能的手段。通過本文介紹的攔截器方案,我們可以:
- 精準定位性能瓶頸
- 建立SQL性能基線
- 主動發(fā)現(xiàn)慢查詢問題
- 量化SQL優(yōu)化效果
技術不是目的,而是解決問題的工具。八年的經(jīng)驗告訴我:最好的優(yōu)化發(fā)生在設計階段,最有效的監(jiān)控是預防性監(jiān)控。
以上就是MyBatis SQL耗時監(jiān)控的最佳實踐的詳細內(nèi)容,更多關于MyBatis SQL耗時監(jiān)控的資料請關注腳本之家其它相關文章!
相關文章
Java關鍵字volatile和synchronized作用和區(qū)別
這篇文章主要為大家詳細介紹了Java關鍵字volatile和synchronized的作用和區(qū)別,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-06-06
java?Stream流常見操作方法(反射,類加載器,類加載,反射)
這篇文章主要介紹了java?Stream流常見操作方法(反射,類加載器,類加載,反射),文章圍繞主題展開詳細的內(nèi)容介紹,展開詳細的內(nèi)容介紹,具有一定參考價值,感興趣的小伙伴可以參考一下2022-06-06
一文系統(tǒng)梳理Java中字符串比較的高頻誤區(qū)
本文圍繞?Java?字符串比較中的高頻誤區(qū)展開,系統(tǒng)梳理?==、equals()、字符串常量池、new?String()、編譯期與運行期拼接、final?常量折疊以及?intern()?的核心規(guī)則,希望幫助?Java?初學者真正理解?String?比較背后的引用關系與常見踩坑點2026-05-05
SpringBoot中@ComponentScan注解過濾排除不加載某個類的3種方法
這篇文章主要給大家介紹了關于SpringBoot中@ComponentScan注解過濾排除不加載某個類的3種方法,文中通過實例代碼介紹的非常詳細,對大家學習或者使用SpringBoot具有一定的參考學習價值,需要的朋友可以參考下2023-07-07

