1. 程式人生 > >數據庫中間件 Sharding-JDBC 源碼分析 —— SQL 解析(一)之語法解析

數據庫中間件 Sharding-JDBC 源碼分析 —— SQL 解析(一)之語法解析

sharding-jdbc

技術分享

關註微信公眾號:【芋艿的後端小屋】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源碼分析文章列表

  2. RocketMQ / MyCAT / Sharding-JDBC 中文註釋源碼 GitHub 地址

  3. 您對於源碼的疑問每條留言將得到認真回復。甚至不知道如何讀源碼也可以請教噢

  4. 新的源碼解析文章實時收到通知。每周更新一篇左右


  • 1. 概述

  • 2. Lexer 詞法解析器

  • 3. Token 詞法標記

    • 3.2.1 Literals.IDENTIFIER 詞法關鍵詞

    • 3.2.2 Literals.VARIABLE 變量

    • 3.2.3 Literals.CHARS 字符串

    • 3.2.4 Literals.HEX 十六進制

    • 3.2.5 Literals.INT 整數

    • 3.2.6 Literals.FLOAT 浮點數

    • 3.1 DefaultKeyword 詞法關鍵詞

    • 3.2 Literals 詞法字面量標記

    • 3.3 Symbol 詞法符號標記

    • 3.4 Assist 詞法輔助標記

  • 4. 彩蛋


1. 概述

SQL 解析引擎,數據庫中間件必備的功能和流程。Sharding-JDBC 在 1.5.0.M1 正式發布時,將 SQL 解析引擎從 Druid 替換成了自研的。新引擎僅解析分片上下文,對於 SQL 采用"半理解"理念,進一步提升性能和兼容性,同時降低了代碼復雜度(不理解沒關系,我們後續會更新文章解釋該優點)。 國內另一款數據庫中間件 MyCAT SQL 解析引擎也是 Druid,目前也在開發屬於自己的 SQL 解析引擎。

可能有同學看到SQL 解析會被嚇到,請淡定,耐心往下看。《SQL 解析》內容我們會分成 5 篇相對簡短的文章,讓大家能夠相對輕松愉快的去理解:

  1. 詞法解析

  2. 插入 SQL 解析

  3. 查詢 SQL 解析

  4. 更新 SQL 解析

  5. 刪除 SQL 解析


SQL 解析引擎parsing 包下,如上圖所見包含兩大組件:

  1. Lexer:詞法解析器。

  2. Parser:SQL解析器。

兩者都是解析器,區別在於 Lexer 只做詞法的解析,不關註上下文,將字符串拆解成 N 個詞法。而 Parser 在 Lexer 的基礎上,還需要理解 SQL 。打個比方:

SQL :SELECT * FROM t_user  Lexer :[SELECT] [ * ] [FROM] [t_user]  Parser :這是一條 [SELECT] 查詢表為 [t_user] ,並且返回 [ * ] 所有字段的 SQL。

不完全懂?沒關系,本文的主角是 Lexer,我們通過源碼一點一點理解。一共 1400 行左右代碼左右,還包含註釋等等,實際更少噢。

2. Lexer 詞法解析器

Lexer 原理順序順序順序 解析 SQL,將字符串拆解成 N 個詞法。

核心代碼如下:

// Lexer.javapublic class Lexer {    /**     * 輸出字符串     * 比如:SQL     */    @Getter    private final String input;    /**     * 詞法標記字典     */    private final Dictionary dictionary;    /**     * 解析到 SQL 的 offset     */    private int offset;    /**     * 當前 詞法標記     */    @Getter    private Token currentToken;    /**     * 分析下一個詞法標記.     *     * @see #currentToken     * @see #offset     */    public final void nextToken() {        skipIgnoredToken();        if (isVariableBegin()) { // 變量            currentToken = new Tokenizer(input, dictionary, offset).scanVariable();        } else if (isNCharBegin()) { // N\            currentToken = new Tokenizer(input, dictionary, ++offset).scanChars();        } else if (isIdentifierBegin()) { // Keyword + Literals.IDENTIFIER            currentToken = new Tokenizer(input, dictionary, offset).scanIdentifier();        } else if (isHexDecimalBegin()) { // 十六進制            currentToken = new Tokenizer(input, dictionary, offset).scanHexDecimal();        } else if (isNumberBegin()) { // 數字(整數+浮點數)            currentToken = new Tokenizer(input, dictionary, offset).scanNumber();        } else if (isSymbolBegin()) { // 符號            currentToken = new Tokenizer(input, dictionary, offset).scanSymbol();        } else if (isCharsBegin()) { // 字符串,例如:"abc"            currentToken = new Tokenizer(input, dictionary, offset).scanChars();        } else if (isEnd()) { // 結束            currentToken = new Token(Assist.END, "", offset);        } else { // 分析錯誤,無符合條件的詞法標記            currentToken = new Token(Assist.ERROR, "", offset);        }        offset = currentToken.getEndPosition();        // System.out.println("| " + currentToken.getLiterals() + " | " + currentToken.getType() + " | " + currentToken.getEndPosition() + " |");    }    /**     * 跳過忽略的詞法標記     * 1. 空格     * 2. SQL Hint     * 3. SQL 註釋     */    private void skipIgnoredToken() {        // 空格        offset = new Tokenizer(input, dictionary, offset).skipWhitespace();        // SQL Hint        while (isHintBegin()) {            offset = new Tokenizer(input, dictionary, offset).skipHint();            offset = new Tokenizer(input, dictionary, offset).skipWhitespace();        }        // SQL 註釋        while (isCommentBegin()) {            offset = new Tokenizer(input, dictionary, offset).skipComment();            offset = new Tokenizer(input, dictionary, offset).skipWhitespace();        }    }}

通過 #nextToken() 方法,不斷解析出 Token(詞法標記)。我們來執行一次,看看 SQL 會被拆解成哪些 Token。

SQL :SELECT i.* FROM t_order o JOIN t_order_item i ON o.order_id=i.order_id WHERE o.user_id=? AND o.order_id=?
literalsTokenType類TokenType值endPosition
SELECTDefaultKeywordSELECT6
iLiteralsIDENTIFIER8
.SymbolDOT9
*SymbolSTAR10
FROMDefaultKeywordFROM15
t_orderLiteralsIDENTIFIER23
oLiteralsIDENTIFIER25
JOINDefaultKeywordJOIN30
torderitemLiteralsIDENTIFIER43
iLiteralsIDENTIFIER45
ONDefaultKeywordON48
oLiteralsIDENTIFIER50
.SymbolDOT51
order_idLiteralsIDENTIFIER59
=SymbolEQ60
iLiteralsIDENTIFIER61
.SymbolDOT62
order_idLiteralsIDENTIFIER70
WHEREDefaultKeywordWHERE76
oLiteralsIDENTIFIER78
.SymbolDOT79
user_idLiteralsIDENTIFIER86
=SymbolEQ87
?SymbolQUESTION88
ANDDefaultKeywordAND92
oLiteralsIDENTIFIER94
.SymbolDOT95
order_idLiteralsIDENTIFIER103
=SymbolEQ104
?SymbolQUESTION105

AssistEND105

眼尖的同學可能看到了 Tokenizer。對的,它是 Lexer 的好基佬,負責分詞

我們來總結下, Lexer#nextToken() 方法裏,使用 #skipIgnoredToken() 方法跳過忽略的 Token,通過 #isXXXX() 方法判斷好下一個 Token 的類型後,交給 Tokenizer 進行分詞返回 Token。此處可以考慮做個優化,不需要每次都 newTokenizer(...) 出來,一個 Lexer 搭配一個 Tokenizer。


由於不同數據庫遵守 SQL 規範略有不同,所以不同的數據庫對應不同的 Lexer。

子 Lexer 通過重寫方法實現自己獨有的 SQL 語法。

3. Token 詞法標記

上文我們已經看過 Token 的例子,一共有 3 個屬性:

  • TokenType type :詞法標記類型

  • String literals :詞法字面量標記

  • int endPosition : literals 在 SQL 裏的結束位置

TokenType 詞法標記類型,一共分成 4 個大類:

  • DefaultKeyword :詞法關鍵詞

  • Literals :詞法字面量標記

  • Symbol :詞法符號標記

  • Assist :詞法輔助標記

3.1 DefaultKeyword 詞法關鍵詞

不同數據庫有自己獨有的詞法關鍵詞,例如 MySQL 熟知的分頁 Limit。

我們以 MySQL 舉個例子,當創建 MySQLLexer 時,會加載 DefaultKeyword 和 MySQLKeyword( OracleLexer、PostgreSQLLexer、SQLServerLexer 同 MySQLLexer )。核心代碼如下:

// MySQLLexer.javapublic final class MySQLLexer extends Lexer {    /**     * 字典     */    private static Dictionary dictionary = new Dictionary(MySQLKeyword.values());    public MySQLLexer(final String input) {        super(input, dictionary);    }}// Dictionary.javapublic final class Dictionary {    /**     * 詞法關鍵詞Map     */    private final Map<String, Keyword> tokens = new HashMap<>(1024);    public Dictionary(final Keyword... dialectKeywords) {        fill(dialectKeywords);    }    /**     * 裝上默認詞法關鍵詞 + 方言詞法關鍵詞     * 不同的數據庫有相同的默認詞法關鍵詞,有有不同的方言關鍵詞     *     * @param dialectKeywords 方言詞法關鍵詞     */    private void fill(final Keyword... dialectKeywords) {        for (DefaultKeyword each : DefaultKeyword.values()) {            tokens.put(each.name(), each);        }        for (Keyword each : dialectKeywords) {            tokens.put(each.toString(), each);        }    }}

Keyword 與 Literals.IDENTIFIER 是一起解析的,我們放在 Literals.IDENTIFIER 處一起分析。

3.2 Literals 詞法字面量標記

Literals 詞法字面量標記,一共分成 6 種:

  • IDENTIFIER :詞法關鍵詞

  • VARIABLE :變量

  • CHARS :字符串

  • HEX :十六進制

  • INT :整數

  • FLOAT :浮點數

3.2.1 Literals.IDENTIFIER 詞法關鍵詞

詞法關鍵詞。例如:表名,查詢字段 等等。

解析 Literals.IDENTIFIER 與 Keyword 核心代碼如下:

// Lexer.javaprivate boolean isIdentifierBegin() {   return isIdentifierBegin(getCurrentChar(0));}private boolean isIdentifierBegin(final char ch) {   return CharType.isAlphabet(ch) || ‘`‘ == ch || ‘_‘ == ch || ‘$‘ == ch;}// Tokenizer.java/*** 掃描標識符.** @return 標識符標記*/public Token scanIdentifier() {   // `字段`,例如:SELECT `id` FROM t_user 中的 `id`   if (‘`‘ == charAt(offset)) {       int length = getLengthUntilTerminatedChar(‘`‘);       return new Token(Literals.IDENTIFIER, input.substring(offset, offset + length), offset + length);   }   int length = 0;   while (isIdentifierChar(charAt(offset + length))) {       length++;   }   String literals = input.substring(offset, offset + length);   // 處理 order / group 作為表名   if (isAmbiguousIdentifier(literals)) {       return new Token(processAmbiguousIdentifier(offset + length, literals), literals, offset + length);   }   // 從 詞法關鍵詞 查找是否是 Keyword,如果是,則返回 Keyword,否則返回 Literals.IDENTIFIER   return new Token(dictionary.findTokenType(literals, Literals.IDENTIFIER), literals, offset + length);}/*** 計算到結束字符的長度** @see #hasEscapeChar(char, int) 處理類似 SELECT a AS `b``c` FROM table。此處連續的 "``" 不是結尾,如果傳遞的是 "`" 會產生誤判,所以加了這個判斷* @param terminatedChar 結束字符* @return 長度*/private int getLengthUntilTerminatedChar(final char terminatedChar) {   int length = 1;   while (terminatedChar != charAt(offset + length) || hasEscapeChar(terminatedChar, offset + length)) {       if (offset + length >= input.length()) {           throw new UnterminatedCharException(terminatedChar);       }       if (hasEscapeChar(terminatedChar, offset + length)) {           length++;       }       length++;   }   return length + 1;}/*** 是否是 Escape 字符** @param charIdentifier 字符* @param offset 位置* @return 是否*/private boolean hasEscapeChar(final char charIdentifier, final int offset) {   return charIdentifier == charAt(offset) && charIdentifier == charAt(offset + 1);}private boolean isIdentifierChar(final char ch) {   return CharType.isAlphabet(ch) || CharType.isDigital(ch) || ‘_‘ == ch || ‘$‘ == ch || ‘#‘ == ch;}/*** 是否是引起歧義的標識符* 例如 "SELECT * FROM group",此時 "group" 代表的是表名,而非詞法關鍵詞** @param literals 標識符* @return 是否*/private boolean isAmbiguousIdentifier(final String literals) {   return DefaultKeyword.ORDER.name().equalsIgnoreCase(literals) || DefaultKeyword.GROUP.name().equalsIgnoreCase(literals);}/*** 獲取引起歧義的標識符對應的詞法標記類型** @param offset 位置* @param literals 標識符* @return 詞法標記類型*/private TokenType processAmbiguousIdentifier(final int offset, final String literals) {   int i = 0;   while (CharType.isWhitespace(charAt(offset + i))) {       i++;   }   if (DefaultKeyword.BY.name().equalsIgnoreCase(String.valueOf(new char[] {charAt(offset + i), charAt(offset + i + 1)}))) {       return dictionary.findTokenType(literals);   }   return Literals.IDENTIFIER;}

3.2.2 Literals.VARIABLE 變量

變量。例如: SELECT@@VERSION

解析核心代碼如下:

// Lexer.java/*** 是否是 變量* MySQL 與 SQL Server 支持* * @see Tokenizer#scanVariable()* @return 是否*/protected boolean isVariableBegin() {   return false;}// Tokenizer.java/*** 掃描變量.* 在 MySQL 裏,@代表用戶變量;@@代表系統變量。* 在 SQLServer 裏,有 @@。** @return 變量標記*/public Token scanVariable() {   int length = 1;   if ([email protected] == charAt(offset + 1)) {       length++;   }   while (isVariableChar(charAt(offset + length))) {       length++;   }   return new Token(Literals.VARIABLE, input.substring(offset, offset + length), offset + length);}

3.2.3 Literals.CHARS 字符串

字符串。例如: SELECT"123"

解析核心代碼如下:

// Lexer.java/*** 是否 N\* 目前 SQLServer 獨有:在 SQL Server 中處理 Unicode 字串常數時,必需為所有的 Unicode 字串加上前置詞 N** @see Tokenizer#scanChars()* @return 是否*/private boolean isNCharBegin() {   return isSupportNChars() && ‘N‘ == getCurrentChar(0) && ‘\‘‘ == getCurrentChar(1);}private boolean isCharsBegin() {   return ‘\‘‘ == getCurrentChar(0) || ‘\"‘ == getCurrentChar(0);}// Tokenizer.java/*** 掃描字符串.** @return 字符串標記*/public Token scanChars() {   return scanChars(charAt(offset));}private Token scanChars(final char terminatedChar) {   int length = getLengthUntilTerminatedChar(terminatedChar);   return new Token(Literals.CHARS, input.substring(offset + 1, offset + length - 1), offset + length);}

3.2.4 Literals.HEX 十六進制

// Lexer.java/*** 是否是 十六進制** @see Tokenizer#scanHexDecimal()* @return 是否*/private boolean isHexDecimalBegin() {   return ‘0‘ == getCurrentChar(0) && ‘x‘ == getCurrentChar(1);}// Tokenizer.java/*** 掃描十六進制數.** @return 十六進制數標記*/public Token scanHexDecimal() {   int length = HEX_BEGIN_SYMBOL_LENGTH;   // 負數   if (‘-‘ == charAt(offset + length)) {       length++;   }   while (isHex(charAt(offset + length))) {       length++;   }   return new Token(Literals.HEX, input.substring(offset, offset + length), offset + length);}

3.2.5 Literals.INT 整數

整數。例如: SELECT*FROM t_user WHERE id=1

Literals.INT 與 Literals.FLOAT 是一起解析的,我們放在 Literals.FLOAT 處一起分析。

3.2.6 Literals.FLOAT 浮點數

浮點數。例如: SELECT*FROM t_user WHERE id=1.0。 浮點數包含幾種:"1.0","1.0F","7.823E5"(科學計數法)。

解析核心代碼如下:

// Lexer.java/*** 是否是 數字* ‘-‘ 需要特殊處理。".2" 被處理成省略0的小數,"-.2" 不能被處理成省略的小數,否則會出問題。* 例如說,"SELECT a-.2" 處理的結果是 "SELECT" / "a" / "-" / ".2"** @return 是否*/private boolean isNumberBegin() {   return CharType.isDigital(getCurrentChar(0)) // 數字           || (‘.‘ == getCurrentChar(0) && CharType.isDigital(getCurrentChar(1)) && !isIdentifierBegin(getCurrentChar(-1)) // 浮點數           || (‘-‘ == getCurrentChar(0) && (‘.‘ == getCurrentChar(0) || CharType.isDigital(getCurrentChar(1))))); // 負數}// Tokenizer.java/*** 掃描數字.* 解析數字的結果會有兩種:整數 和 浮點數.** @return 數字標記*/public Token scanNumber() {   int length = 0;   // 負數   if (‘-‘ == charAt(offset + length)) {       length++;   }   // 浮點數   length += getDigitalLength(offset + length);   boolean isFloat = false;   if (‘.‘ == charAt(offset + length)) {       isFloat = true;       length++;       length += getDigitalLength(offset + length);   }   // 科學計數表示,例如:SELECT 7.823E5   if (isScientificNotation(offset + length)) {       isFloat = true;       length++;       if (‘+‘ == charAt(offset + length) || ‘-‘ == charAt(offset + length)) {           length++;       }       length += getDigitalLength(offset + length);   }   // 浮點數,例如:SELECT 1.333F   if (isBinaryNumber(offset + length)) {       isFloat = true;       length++;   }   return new Token(isFloat ? Literals.FLOAT : Literals.INT, input.substring(offset, offset + length), offset + length);}

這裏要特別註意下:"-"。在數字表達實例,可以判定為 負號 和 減號(不考慮科學計數法)。

  • ".2" 解析結果是 ".2"

  • "-.2" 解析結果不能是 "-.2",而是 "-" 和 ".2"。

3.3 Symbol 詞法符號標記

詞法符號標記。例如:"{", "}", ">=" 等等。

解析核心代碼如下:

// Lexer.java/*** 是否是 符號** @see Tokenizer#scanSymbol()* @return 是否*/private boolean isSymbolBegin() {   return CharType.isSymbol(getCurrentChar(0));}// CharType.java/*** 判斷是否為符號.** @param ch 待判斷的字符* @return 是否為符號*/public static boolean isSymbol(final char ch) {   return ‘(‘ == ch || ‘)‘ == ch || ‘[‘ == ch || ‘]‘ == ch || ‘{‘ == ch || ‘}‘ == ch || ‘+‘ == ch || ‘-‘ == ch || ‘*‘ == ch || ‘/‘ == ch || ‘%‘ == ch || ‘^‘ == ch || ‘=‘ == ch           || ‘>‘ == ch || ‘<‘ == ch || ‘~‘ == ch || ‘!‘ == ch || ‘?‘ == ch || ‘&‘ == ch || ‘|‘ == ch || ‘.‘ == ch || ‘:‘ == ch || ‘#‘ == ch || ‘,‘ == ch || ‘;‘ == ch;}// Tokenizer.java/*** 掃描符號.** @return 符號標記*/public Token scanSymbol() {   int length = 0;   while (CharType.isSymbol(charAt(offset + length))) {       length++;   }   String literals = input.substring(offset, offset + length);   // 倒序遍歷,查詢符合條件的 符號。例如 literals = ";;",會是拆分成兩個 ";"。如果基於正序,literals = "<=",會被解析成 "<" + "="。   Symbol symbol;   while (null == (symbol = Symbol.literalsOf(literals))) {       literals = input.substring(offset, offset + --length);   }   return new Token(symbol, literals, offset + length);}

3.4 Assist 詞法輔助標記

Assist 詞法輔助標記,一共分成 2 種:

  • END :分析結束

  • ERROR :分析錯誤。

4. 彩蛋

老鐵,是不是比想象中簡單一些?!繼續加油寫 Parser 相關的文章!來一波微信公眾號關註吧。


Sharding-JDBC 正在收集使用公司名單:傳送門。 你的登記,會讓更多人參與和使用 Sharding-JDBC。Sharding-JDBC 也會因此,能夠覆蓋更廣的場景。登記吧,少年!


我創建了一個微信群【源碼圈】,希望和大家分享交流讀源碼的經驗。
讀源碼先難後易,掌握方法後,可以做更有深度的學習。
而且掌握方法並不難噢。
加群方式:微信公眾號發送關鍵字【qun】。


本文出自 “芋艿的後端小屋” 博客,請務必保留此出處http://yunai.blog.51cto.com/2625549/1950288

數據庫中間件 Sharding-JDBC 源碼分析 —— SQL 解析(一)之語法解析