MyBatis SQL耗時(shí)監(jiān)控的最佳實(shí)踐
一、為什么我們需要關(guān)注SQL執(zhí)行時(shí)間?
在我多年的開(kāi)發(fā)經(jīng)歷中,數(shù)據(jù)庫(kù)性能瓶頸往往是系統(tǒng)優(yōu)化的關(guān)鍵點(diǎn)。特別是在微服務(wù)架構(gòu)和高并發(fā)場(chǎng)景下,一個(gè)未優(yōu)化的SQL可能拖垮整個(gè)系統(tǒng)。以下是幾個(gè)典型的業(yè)務(wù)場(chǎng)景:
- 生產(chǎn)環(huán)境性能問(wèn)題排查:用戶突然反饋系統(tǒng)變慢,需要快速定位慢SQL
- 接口性能優(yōu)化:API響應(yīng)時(shí)間超過(guò)預(yù)期,需要分析數(shù)據(jù)庫(kù)操作耗時(shí)
- 慢SQL監(jiān)控:建立系統(tǒng)化的慢查詢監(jiān)控機(jī)制
- SQL優(yōu)化驗(yàn)證:驗(yàn)證SQL優(yōu)化措施的實(shí)際效果
二、技術(shù)方案選型:為什么選擇MyBatis攔截器?
在Java生態(tài)中,我們有多種方式可以實(shí)現(xiàn)SQL耗時(shí)統(tǒng)計(jì):
| 方案 | 優(yōu)點(diǎn) | 缺點(diǎn) |
|---|---|---|
| MyBatis攔截器 | 原生支持、侵入性低、精準(zhǔn)控制 | 需要理解MyBatis內(nèi)部機(jī)制 |
| AOP切面 | 通用性強(qiáng)、與ORM解耦 | 無(wú)法獲取SQL語(yǔ)句細(xì)節(jié) |
| 日志框架 | 簡(jiǎn)單直接 | 日志量大、難以結(jié)構(gòu)化處理 |
| 數(shù)據(jù)庫(kù)監(jiān)控 | 無(wú)需代碼改動(dòng) | 脫離應(yīng)用上下文、配置復(fù)雜 |
作為有經(jīng)驗(yàn)的開(kāi)發(fā)者,我推薦使用MyBatis攔截器方案。它提供了最直接的SQL執(zhí)行切入點(diǎn),能獲取最豐富的執(zhí)行上下文信息,且對(duì)業(yè)務(wù)代碼零侵入。
三、核心實(shí)現(xiàn):從攔截器到耗時(shí)統(tǒng)計(jì)
3.1 實(shí)現(xiàn)原理
MyBatis攔截器基于責(zé)任鏈模式,我們可以通過(guò)實(shí)現(xiàn)Interceptor接口,在以下關(guān)鍵點(diǎn)插入邏輯:
Executor.update()- 攔截增刪改操作Executor.query()- 攔截查詢操作
3.2 完整代碼實(shí)現(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í)行目標(biāo)方法
} finally {
long cost = System.currentTimeMillis() - start;
logSqlPerformance(sqlId, sql, parameterObject, cost);
}
}
private void logSqlPerformance(String sqlId, String sql,
Object params, long cost) {
// 基礎(chǔ)日志
String logMsg = String.format("SQL執(zhí)行耗時(shí): %dms | ID: %s | SQL: %s",
cost, sqlId, sql);
// 參數(shù)日志(敏感數(shù)據(jù)需脫敏)
if (showParameters && params != null) {
String paramStr = params.toString();
// 簡(jiǎn)單脫敏處理(實(shí)際項(xiàng)目應(yīng)使用專業(yè)脫敏工具)
paramStr = paramStr.replaceAll("(\"password\":\")([^\"]+)(\")",
"$1****$3");
logMsg += " | 參數(shù): " + paramStr;
}
// 分級(jí)日志輸出
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 關(guān)鍵代碼解析
- @Intercepts注解:定義攔截的類和方法簽名
- BoundSql獲取:通過(guò)MappedStatement獲取實(shí)際執(zhí)行的SQL
- 參數(shù)脫敏處理:防止敏感數(shù)據(jù)(如密碼)泄露到日志
- 分級(jí)日志輸出:根據(jù)耗時(shí)長(zhǎng)短使用不同日志級(jí)別
- 可配置化:通過(guò)setProperties方法實(shí)現(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)境進(jìn)階技巧
4.1 性能優(yōu)化策略
異步日志輸出:避免日志IO阻塞業(yè)務(wù)線程
private final ExecutorService logExecutor =
Executors.newSingleThreadExecutor();
private void logSqlPerformance(...) {
logExecutor.submit(() -> {
// 日志記錄邏輯
});
}
采樣率控制:高并發(fā)場(chǎng)景下按比例采樣
// 在intercept方法中加入
if (ThreadLocalRandom.current().nextDouble() < 0.2) {
// 只記錄20%的請(qǐng)求
}
4.2 監(jiān)控系統(tǒng)集成
將SQL耗時(shí)數(shù)據(jù)發(fā)送到監(jiān)控系統(tǒng)(如Prometheus):
private void recordMetrics(String sqlId, long cost) {
// Prometheus指標(biāo)記錄
Summary.builder("sql_execution_time")
.tag("sql_id", sqlId)
.register()
.observe(cost);
// 慢查詢計(jì)數(shù)器
if (cost > slowQueryThreshold) {
Counter.builder("slow_sql_count")
.tag("sql_id", sqlId)
.register()
.inc();
}
}
4.3 可視化與告警
結(jié)合Grafana等工具創(chuàng)建SQL性能儀表盤(pán):
- 按SQL ID統(tǒng)計(jì)平均耗時(shí)
- 慢查詢發(fā)生頻率
- SQL執(zhí)行次數(shù)統(tǒng)計(jì)
設(shè)置告警規(guī)則:
- 單個(gè)SQL平均耗時(shí)連續(xù)5分鐘 > 500ms
- 慢查詢頻率每分鐘 > 10次
五、經(jīng)驗(yàn)總結(jié)與避坑指南
在八年開(kāi)發(fā)實(shí)踐中,我總結(jié)了以下重要經(jīng)驗(yàn):
謹(jǐn)慎處理參數(shù)日志:
if (paramStr.length() > 1000) {
paramStr = paramStr.substring(0, 1000) + "...[truncated]";
}
- 必須進(jìn)行敏感信息脫敏
- 大對(duì)象參數(shù)截?cái)嗵幚?/li>
區(qū)分環(huán)境配置:
- 開(kāi)發(fā)環(huán)境:記錄詳細(xì)SQL和參數(shù)
- 生產(chǎn)環(huán)境:僅記錄慢查詢,關(guān)閉參數(shù)打印
線程安全考量:
- 攔截器是單例的,所有狀態(tài)變量必須線程安全
- 使用ThreadLocal存儲(chǔ)耗時(shí)上下文
避免過(guò)度監(jiān)控:
- 核心業(yè)務(wù)SQL重點(diǎn)監(jiān)控
- 低頻管理后臺(tái)SQL可降低監(jiān)控強(qiáng)度
與其他監(jiān)控工具協(xié)同:
- 整合鏈路追蹤(如SkyWalking)的TraceID
- 與APM工具協(xié)同工作
六、結(jié)論
記錄MyBatis SQL執(zhí)行耗時(shí)不是目的,而是優(yōu)化系統(tǒng)性能的手段。通過(guò)本文介紹的攔截器方案,我們可以:
- 精準(zhǔn)定位性能瓶頸
- 建立SQL性能基線
- 主動(dòng)發(fā)現(xiàn)慢查詢問(wèn)題
- 量化SQL優(yōu)化效果
技術(shù)不是目的,而是解決問(wèn)題的工具。八年的經(jīng)驗(yàn)告訴我:最好的優(yōu)化發(fā)生在設(shè)計(jì)階段,最有效的監(jiān)控是預(yù)防性監(jiān)控。
以上就是MyBatis SQL耗時(shí)監(jiān)控的最佳實(shí)踐的詳細(xì)內(nèi)容,更多關(guān)于MyBatis SQL耗時(shí)監(jiān)控的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
SpringBoot項(xiàng)目在IntelliJ IDEA中如何實(shí)現(xiàn)熱部署
spring-boot-devtools是一個(gè)為開(kāi)發(fā)者服務(wù)的一個(gè)模塊,其中最重要的功能就是自動(dòng)應(yīng)用代碼更改到最新的App上面去。,這篇文章主要介紹了SpringBoot項(xiàng)目在IntelliJ IDEA中如何實(shí)現(xiàn)熱部署,感興趣的小伙伴們可以參考一下2018-07-07
Java實(shí)現(xiàn)把文件及文件夾壓縮成zip
這篇文章主要介紹了Java實(shí)現(xiàn)把文件及文件夾壓縮成zip,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-03-03
Java?Mybatis的初始化之Mapper.xml映射文件的詳解
這篇文章主要介紹了Java?Mybatis的初始化之Mapper.xml映射文件的詳解,解析完全局配置文件后接下來(lái)就是解析Mapper文件了,它是通過(guò)XMLMapperBuilder來(lái)進(jìn)行解析的2022-08-08
基于FeignClient調(diào)用超時(shí)的處理方案
這篇文章主要介紹了基于FeignClient調(diào)用超時(shí)的處理方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07
springboot運(yùn)行時(shí)新增/更新外部接口的實(shí)現(xiàn)方法
這篇文章主要介紹了springboot運(yùn)行時(shí)新增/更新外部接口的實(shí)現(xiàn)方法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-03-03
springboot+websocket實(shí)現(xiàn)并發(fā)搶紅包功能
本文主要介紹了springboot+websocket實(shí)現(xiàn)并發(fā)搶紅包功能,主要包含了4種步驟,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-12-12
Java并發(fā)編程必備之Synchronized關(guān)鍵字深入解析
本文我們深入探索了Java中的Synchronized關(guān)鍵字,包括其互斥性和可重入性的特性,文章詳細(xì)介紹了Synchronized的三種使用方式:修飾代碼塊、修飾普通方法和修飾靜態(tài)方法,感興趣的朋友一起看看吧2025-04-04
深入淺析drools中Fact的equality?modes
這篇文章主要介紹了drools中Fact的equality?modes的相關(guān)知識(shí),本文通過(guò)圖文實(shí)例代碼相結(jié)合給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-05-05
SpringBoot配置文件中系統(tǒng)環(huán)境變量存在特殊字符的處理方式
這篇文章主要介紹了SpringBoot配置文件中系統(tǒng)環(huán)境變量存在特殊字符的處理方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02

