Sharding-JDBC 原始碼之 SQL 解析

sky凌亂的微笑發表於2020-12-24

基於 SpringBoot2.4 快速搭建一個 Demo,主要 jar 包依賴版本分別是:

  • io.shardingsphere.sharding-jdbc-spring-boot-starter:3.0.0.M1
  • com.alibaba.druid-spring-boot-starter:1.1.17
  • org.mybatis.spring.boot.mybatis-spring-boot-starter:2.1.4

在執行 SQL 時,Sharding-JDBC 需要根據啟動時獲取到的資料來源、路由等配置檔案獲取真正執行的 SQL, 那在獲取真正執行的 語句之前還需要對 SQL 進行解析、路由,今天我們來看下 Sharding-JDBC 是如何解析 SQL 語句的。

本文以查詢語句為例進行分析,資料來源及分庫分表配置和之前保持一致。
從原始碼看,SQL 解析工作主要在 Sharding-JDBC 原始碼之 SQL 解析 的 parsing 包中:
在這裡插入圖片描述

  • cache 包:快取解析過 SQL 語句;
  • lexer 包:詞法分析,封裝不同方言(MysqlOracle 等)的解析器,使用 LexerEngine 完成詞法分析;
  • parser 包:SQL 解析引擎,根據 SQL 語句的型別完成解析工作。

在看具體執行流程前,先了解下 parsing 封裝的集中 SQL 語句型別:

  • dal:資料訪問語言(Data Access Language) 例: use、desc、describe、show
  • ddl:資料定義語言(Data Definition Language) 例:create、alter、drop、truncate
  • dml:資料操作語言(Data Manipulation Language) 例: insert、update、delete
  • dql:資料查詢語言(Data Query Language) 例: select
  • tcl:事務控制語言(Transaction Control Language) 例: set、commit、rollback、savepoint、begin。

在執行語句時,需要進行路由,而進行路由,需要對 SQL 進行解析,下面看下解析的具體執行流程:

獲取 SQL 解析引擎

由於我們使用的是標準分片策略,所以獲取到的解析引擎是 SQLParsingEngine,而該引擎中的關鍵方法是 parse

public final class ParsingResultCache {
    // 省略部分程式碼
    // 邏輯 SQL 快取    
    private volatile Map<String, SQLStatement> cache = new WeakHashMap<>(65535, 1);
    // 省略部分程式碼
}

public final class SQLParsingEngine {
    ......
    
    public SQLStatement parse(final boolean useCache) {
        // 從快取中獲取解析結果
        Optional<SQLStatement> cachedSQLStatement = getSQLStatementFromCache(useCache);
        // 快取中存在則直接獲取返回解析結果
        if (cachedSQLStatement.isPresent()) {
            return cachedSQLStatement.get();
        }
        
        // 根據資料庫型別獲取具體方言詞法分析器
        LexerEngine lexerEngine = LexerEngineFactory.newInstance(dbType, sql);
        lexerEngine.nextToken();
        // 根據邏輯 SQL 型別獲取具體語句解析器,並解析語言
        SQLStatement result = SQLParserFactory.newInstance(dbType, lexerEngine.getCurrentToken().getType(), shardingRule, lexerEngine, shardingMetaData).parse();
        // 將解析結果快取到 cache 物件中
        if (useCache) {
            ParsingResultCache.getInstance().put(sql, result);
        }
        return result;
    }
    
    // 從快取中獲取解析結果
    private Optional<SQLStatement> getSQLStatementFromCache(final boolean useCache) {
        return useCache ? Optional.fromNullable(ParsingResultCache.getInstance().getSQLStatement(sql)) : Optional.<SQLStatement>absent();
    }
}    
  1. ParsingResultCache 類中的 cache 物件主要用來快取解析過的邏輯 SQL,避免重複解析。同時 cache 使用的是 WeakHashMap,這樣可以保證不用手動釋放引用且保證記憶體不被過度使用,因為弱引用物件只能存活到下一次 GC 之前,
    快取的 key 形如:select goods_name from t_goods where goods_id=?
  2. 本例中,是 select 查詢語句,所以獲取的是 MySQLSelectParser,執行該解析器的 parseInternal 方法,其他型別的語句根據解析器的不同,在執行 parse 方法時進入具體的實現類中。

解析 SQL

解析邏輯 SQL,將解析結果封裝為 SelectStatement 物件

    public MySQLSelectParser(final ShardingRule shardingRule, final LexerEngine lexerEngine, final ShardingMetaData shardingMetaData) {
        // 1. 建立 DQL 門面解析器
        super(shardingRule, lexerEngine, new MySQLSelectClauseParserFacade(shardingRule, lexerEngine), shardingMetaData);
        selectOptionClauseParser = new MySQLSelectOptionClauseParser(lexerEngine);
        limitClauseParser = new MySQLLimitClauseParser(lexerEngine);
    }
    
    /**
     * 2. 解析具體邏輯 SQL 
     */
    protected void parseInternal(final SelectStatement selectStatement) {
        parseDistinct();
        parseSelectOption();
        // 解析待查詢的列,獲取資料庫欄位名和欄位別名
        parseSelectList(selectStatement, getItems());
        // 解析獲取邏輯表
        parseFrom(selectStatement);
        // 獲取查詢條件
        parseWhere(getShardingRule(), selectStatement, getItems());
        parseGroupBy(selectStatement);
        parseHaving();
        parseOrderBy(selectStatement);
        parseLimit(selectStatement);
        parseSelectRest();
    }
  • 建立 MySQLSelectClauseParserFacade 物件,初始化 DQL 語句門面解析器,為下面 parseInternal 做準備;
  • 解析具體邏輯 SQL,將語句按照 token 型別封裝為 SelectStatement 物件,為後面 SQL 改寫做準備。

總結

  1. 具體的詞法分析並沒有展開,主要是根據資料庫型別獲取對應的詞法解析器,本文以 Mysql 為例,因此獲取的是 MySQLLexer,接下來詞法分析引擎 LexerEngine 獲取到第一個有效的 token,並根據此 token 獲取 SQL 解析器,本例中邏輯 SQL 第一個有效的 token 是 select,所以獲得的解析器是 MySQLSelectParser
  2. 整體上,語句解析過程還是很簡單的,就是將邏輯 SQL 按照 token 型別解析為 SQLStatement,為後面 SQL 路由改寫做準備。不過,詞法分析還是略複雜,有興趣可自行 debug 跟蹤學習。

相關文章