一、背景
Durid是一款應用比較廣泛的數據庫連接池,其性能優越、監控機制強大,並且還支持通過filter的機制進行擴展。
Druid自帶一個StatFilter可以進行慢sql記錄,但我在使用中發現一些不足:
- 此Filter打印日誌爲ERROR級別,當系統監控錯誤日誌時可能會頻繁觸發告警,
- 判斷閾值只能在配置文件中進行設置,不支持動態調整,
- 只實現了日誌打印,而不能進行後續統計等功能
因此嘗試使用一個自定義的Filter來記錄慢Sql,並實現動態開關及閾值調整。
自定義Filter除了實現特定方法外,還需要將其加入DruidDataSource的Filter鏈中。一般情況下需要手動add到dataSource的filter集合中。而在SpringBoot環境下,只要將自定義的Filter聲明爲Component即可自動裝配到FilterChain中不需要額外配置。
參考文章:
自定義Druid的攔截器
Nacos快速入門:啓動Nacos Server(控制檯服務)
Nacos快速入門:整合SpringBoot實現配置管理和服務發現
二、自定義Druid Filter
1. 主要步驟:
- 繼承FilterEventAdapter類,並實現4個方法,後序攔截所有類型的sql執行:
- statementExecuteUpdateAfter()
- statementExecuteQueryAfter()
- statementExecuteAfter()
- statementExecuteBatchAfter()
- 聲明兩個屬性:logSwitch、slowSqlMillis,加上nacos的註解用於動態配置
- 新增方法,在執行完sql後對執行時間進行判斷,並記錄sql語句及參數(這裏主要參照了StatFilter中的代碼),打印爲warn日誌
- 這裏還可自行擴展,將sql記錄發送到mq、es、或其他數據庫等,進行後續統計監控
- 開啓Druid自帶StatFilter:
- application.propertis:
spring.datasource.druid.filter.stat.enabled=true
- application.propertis:
2. 完整代碼:
@Slf4j
@Component
public class CustomDruidStatLogFilter extends FilterEventAdapter {
private static final Pattern LINE_BREAK = Pattern.compile("\n");
private static final Pattern BLANKS = Pattern.compile(" +");
private static final String BLANK = " ";
/**
* 開啓狀態
* (此註解表示此屬性爲nacos動態屬性,對應配置中logSwitch對應的值,默認爲true,且支持動態刷新)
*/
@NacosValue(value = "${logSwitch:true}", autoRefreshed = true)
private boolean logSwitch;
/**
* 慢sql判斷閾值(毫秒)
*/
@NacosValue(value = "${slowSqlMillis:100}", autoRefreshed = true)
private long slowSqlMillis;
@Override
protected void statementExecuteUpdateAfter(StatementProxy statement, String sql, int updateCount) {
internalAfterStatementExecute(statement);
}
@Override
protected void statementExecuteQueryAfter(StatementProxy statement, String sql, ResultSetProxy resultSet) {
internalAfterStatementExecute(statement);
}
@Override
protected void statementExecuteAfter(StatementProxy statement, String sql, boolean firstResult) {
internalAfterStatementExecute(statement);
}
@Override
protected void statementExecuteBatchAfter(StatementProxy statement, int[] result) {
internalAfterStatementExecute(statement);
}
/**
* 核心記錄方法:判斷記錄慢sql
*/
private void internalAfterStatementExecute(StatementProxy statement) {
if (logSwitch) {
if (statement.getSqlStat() != null) {
long nanos = System.nanoTime() - statement.getLastExecuteStartNano();
long millis = nanos / (1000 * 1000);
if (millis >= slowSqlMillis) {
String slowParameters = buildSlowParameters(statement);
String sql = statement.getLastExecuteSql();
sql = LINE_BREAK.matcher(sql).replaceAll(BLANK);
sql = BLANKS.matcher(sql).replaceAll(BLANK);
// 打印日誌。還可自行替換爲使用數據庫等方式來保存,用於後續統計
log.warn("slow sql [" + millis + "] millis, sql: [" + sql + "], params: "
+ slowParameters);
}
}
}
}
/**
* 組裝查詢參數
*/
private String buildSlowParameters(StatementProxy statement) {
JSONWriter out = new JSONWriter();
out.writeArrayStart();
for (int i = 0, parametersSize = statement.getParametersSize(); i < parametersSize; ++i) {
JdbcParameter parameter = statement.getParameter(i);
if (i != 0) {
out.writeComma();
}
if (parameter == null) {
continue;
}
Object value = parameter.getValue();
if (value == null) {
out.writeNull();
} else if (value instanceof String) {
String text = (String) value;
if (text.length() > 100) {
out.writeString(text.substring(0, 97) + "...");
} else {
out.writeString(text);
}
} else if (value instanceof Number) {
out.writeObject(value);
} else if (value instanceof java.util.Date) {
out.writeObject(value);
} else if (value instanceof Boolean) {
out.writeObject(value);
} else if (value instanceof InputStream) {
out.writeString("<InputStream>");
} else if (value instanceof NClob) {
out.writeString("<NClob>");
} else if (value instanceof Clob) {
out.writeString("<Clob>");
} else if (value instanceof Blob) {
out.writeString("<Blob>");
} else {
out.writeString('<' + value.getClass().getName() + '>');
}
}
out.writeArrayEnd();
return out.toString();
}
}
三、測試效果
1. 準備
1.1 建表
主要保存NAME
和PHONE
兩個字段,其中PHONE
創建了索引
CREATE TABLE `USER`(
`ID` INT(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵ID',
`PHONE` VARCHAR(60) NOT NULL DEFAULT '' COMMENT '手機號',
`NAME` VARCHAR(60) NOT NULL DEFAULT '' COMMENT '姓名',
`CREATE_TIME` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',
`UPDATE_TIME` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新時間',
PRIMARY KEY (`ID`),
KEY `idx_phone` (`PHONE`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT ='用戶表'
1.2 初始化數據
向USER
表插入50000條數據,數據格式如下。
1.3 初始化nacos配置
通過nacos控制檯發佈配置:
1.4 查詢代碼
測試使用了Mybatis-plus進行查詢
使用定時任務,每隔5s分別使用隨機的name
和phone
去查詢USER
表
@Component
@Slf4j
public class QueryDataJob {
@Autowired
private UserMapper userMapper;
/** 使用隨機姓名查詢 */
@Scheduled(fixedRate = 5000)
public void queryByName() {
int randomIndex = RandomUtil.randomInt(0, 100000);
String randomName = "TestName" + randomIndex;
User query = new User();
query.setName(randomName);
List<User> result = userMapper.select(query);
log.info("query by name,result:{}", JSONUtil.toJsonStr(result));
}
/** 使用隨機手機號(建有索引)查詢 */
@Scheduled(fixedRate = 5000)
public void queryByPhone(){
int randomIndex = RandomUtil.randomInt(0, 100000);
String randomName = "TestName" + randomIndex;
User query = new User();
query.setPhone(randomName);
List<User> result = userMapper.select(query);
log.info("query by phone,result:{}", JSONUtil.toJsonStr(result));
}
}
2. 慢SQL記錄
由於使用name
查詢時,沒有走索引,sql執行時間較長,超過了100ms,通過CustomDruidStatLogFilter
的warn日誌記錄下來:
3. 動態調整
嘗試調整nacos配置,來改變記錄結果:
3.1 開啓/關閉
-
在nacos控制檯將logSwitch設置爲false:
-
發佈配置後,慢sql記錄不再打印日誌,說明logSwitch配置動態調整生效:
3.2 調整判斷閾值
- 將logSwitch開啓,並將判斷閾值設置爲1ms:
-
兩個查詢都被記錄了下來,閾值動態調整生效:
-
全部測試完成