手把手教你自定義一個Druid Filter記錄sql,並結合Nacos實現動態開關和判斷閾值調整

一、背景

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. 主要步驟:

  1. 繼承FilterEventAdapter類,並實現4個方法,後序攔截所有類型的sql執行:
    • statementExecuteUpdateAfter()
    • statementExecuteQueryAfter()
    • statementExecuteAfter()
    • statementExecuteBatchAfter()
  2. 聲明兩個屬性:logSwitch、slowSqlMillis,加上nacos的註解用於動態配置
  3. 新增方法,在執行完sql後對執行時間進行判斷,並記錄sql語句及參數(這裏主要參照了StatFilter中的代碼),打印爲warn日誌
    • 這裏還可自行擴展,將sql記錄發送到mq、es、或其他數據庫等,進行後續統計監控
  4. 開啓Druid自帶StatFilter:
    • application.propertis:spring.datasource.druid.filter.stat.enabled=true

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 建表

主要保存NAMEPHONE兩個字段,其中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條數據,數據格式如下。
image-20200628173036068

1.3 初始化nacos配置

通過nacos控制檯發佈配置:

image-20200628202207915

1.4 查詢代碼

測試使用了Mybatis-plus進行查詢

使用定時任務,每隔5s分別使用隨機的namephone去查詢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日誌記錄下來:
image-20200628201013779

3. 動態調整

嘗試調整nacos配置,來改變記錄結果:

3.1 開啓/關閉

  • 在nacos控制檯將logSwitch設置爲false:
    image-20200628202323731

  • 發佈配置後,慢sql記錄不再打印日誌,說明logSwitch配置動態調整生效:
    image-20200628201719367

3.2 調整判斷閾值

  • 將logSwitch開啓,並將判斷閾值設置爲1ms:

image-20200628202539996

  • 兩個查詢都被記錄了下來,閾值動態調整生效:
    image-20200628202526504

  • 全部測試完成

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章