輕量級數(shù)據(jù)庫中間件Sharding-JDBC源碼分析SQL 解析之更新SQL

Sharding-JDBC架構圖如下:

左邊部分是部署架構圖,右邊部分則是核心邏輯架構圖扭倾。

使用Sharding-JDBC淀零,性能是大家最關心的問題。

在數(shù)據(jù)量一致的情況下膛壹,使用Sharding-JDBC和原生JDBC的性能測試報告如下:

查詢操作:Sharding-JDBC的TPS為JDBC的TPS的99.8%窑滞。

插入操作:Sharding-JDBC的TPS為JDBC的TPS的90.2%。

更新操作:Sharding-JDBC的TPS為JDBC的TPS的93.1%恢筝。

可以看到,Sharding-JDBC在查詢中的性能損失非常低巨坊,插入和更新略高撬槽。

將單表的數(shù)據(jù)拆分為二,放入兩個表中趾撵,使用Sharding-JDBC和原生JDBC的性能測試報告如下:

查詢操作:TPS雙庫比單庫可以增加大約94%的性能侄柔。

插入操作:TPS雙庫比單庫可以增加大約60%的性能共啃。

更新操作:TPS雙庫比單庫可以增加大約89%的性能。

結果表明暂题,Sharding-JDBC可有效利用水平擴展大幅度提升性能移剪。

下面我將按照模塊深度剖析Sharding-JDBC的詳細功能和主要實現(xiàn),請大家和我一起探索與評估它的水有多深薪者。

分片規(guī)則配置

Sharding-JDBC的分片策略配置是自定義的纵苛,因此可以通過編程的方式最大限度的靈活調整。它并不僅支持=運算符分片言津,可支持BETWEEN和IN的運算符分片攻人,支持將一條邏輯SQL最終散落至多個數(shù)據(jù)節(jié)點。同時支持多分片鍵悬槽,例如:根據(jù)用戶ID分庫怀吻,訂單ID分表這種分庫分表結合的分片策略;或根據(jù)年分庫初婆,月份+用戶區(qū)域ID分表這樣的多片鍵分片蓬坡。

通過編程的方式定制分片規(guī)則雖然靈活,但配置起來略顯繁瑣磅叛。因此Sharding-JDBC又提供了Inline表達式編寫分片策略的方式屑咳,用于配置集中化,以避免配置散落在配置文件和代碼中的情況宪躯。此外乔宿,它還提供了定制化的Spring命名空間和YAML進一步簡化配置。

JDBC規(guī)范重寫

Sharding-JDBC對JDBC規(guī)范的重寫思路是針對DataSource访雪、Connection详瑞、Statement、PreparedStatement和ResultSet這5個核心接口封裝臣缀,將多個實現(xiàn)類集合納入Sharding-JDBC實現(xiàn)類管理坝橡。分布式主鍵也屬于JDBC協(xié)議的一部分。

Sharding-JDBC盡量最大化實現(xiàn)JDBC協(xié)議精置,但分布式畢竟與原生JDBC不同计寇,所以目前仍有未實現(xiàn)的接口,包括游標脂倦,存儲過程番宁、SavePoint以及向前遍歷和修改ResultSet等不太常用的功能。此外赖阻,為了保證兼容性蝶押,并未實現(xiàn)JDBC 4.1及其后發(fā)布的接口(如:DBCP 1.x版本不支持JDBC 4.1)。

SQL解析

SQL解析作為分庫分表類產品的核心火欧,性能和兼容性是最重要的衡量指標棋电。目前常見的SQL解析器主要有fdb茎截,jsqlparser和Druid。Sharding-JDBC1.4.x之前的版本使用Druid作為SQL解析器赶盔,經實際測試企锌,它的性能遠超其它解析器。

從1.5.x版本開始于未,Sharding-JDBC采用完全自研的SQL解析引擎撕攒。由于目的不同,它并不需要將SQL轉為AST語法樹沉眶,也無需通過Visitor的方式二次遍歷打却。它采用對SQL“半理解”的方式,僅提煉分片需要關注的上下文谎倔,因此SQL解析的性能和容錯性得到了進一步的提高柳击。

SQL解析模塊由Lexer和Parser兩個模塊組成。Lexer用于將SQL拆解為Token片习,并將其歸類為關鍵詞捌肴,表達式,字面量和操作符藕咏。Parser則用于理解SQL和提煉分片上下文状知,并標記可能需要改寫的位置。分片上下文包含SELECTItems孽查、表信息饥悴、分片條件、自增主鍵信息盲再、排序信息西设、分組信息和Limit信息。一次解析過程是不可逆的答朋,一個個Token的依次解析贷揽,因此解析性能很高。由于各種數(shù)據(jù)庫的SQL差異很大梦碗,因此在解析模塊對每種數(shù)據(jù)庫提供方言的支持禽绪。

Sharding-JDBC支持各種連接、聚合洪规、排序印屁、分組以及分頁的解析,并且可以有限度的支持子查詢斩例。

SQL路由

SQL路由是根據(jù)分片規(guī)則配置以及解析上下文中的分片條件库车,將SQL定位至真正的數(shù)據(jù)源。它又分為直接路由樱拴、簡單路由和笛卡爾積路由柠衍。

滿足直接路由的條件比較苛刻,如果通過Hint(通過HintAPI直接指定路由至庫表)方式分片晶乔,且僅分庫珍坊,則無需SQL解析和結果歸并。因此它的SQL兼容性最好正罢,可以執(zhí)行包括子查詢阵漏、OR、UNION等復雜情況的任意SQL翻具。

簡單路由是Sharding-JDBC最推薦使用的分片方式履怯,它是指不包含JOIN或僅包含Binding表JOIN的SQL。Binding表是指使用同樣的分片鍵和分片規(guī)則的一組表裆泳,也就是說任何情況下叹洲,Binding表的分片結果應與主表一致。例如:order表和order_item表工禾,都根據(jù)order_id分片运提,結果應是order_1與order_item_1成對出現(xiàn)。這樣的關聯(lián)查詢和單表查詢復雜度和性能相當闻葵。如果分片條件不是等于民泵,而是BETWEEN或IN,則路由結果不一定落入單庫(表)槽畔,因此一條邏輯SQL最終可能拆分為多條SQL語句栈妆。

笛卡爾積查詢最為復雜,因為無法根據(jù)Binding關系定位分片規(guī)則的一致性厢钧,所以非Binding表的關聯(lián)查詢需要拆解為笛卡爾積組合執(zhí)行鳞尔。查詢性能較低,而且數(shù)據(jù)庫連接數(shù)較高坏快,需謹慎使用铅檩。

SQL改寫

SQL改寫模塊的用途是將邏輯SQL改寫為可以分布式執(zhí)行的SQL。在Sharding-JDBC 1.5.x版本莽鸿,SQL改寫進行了調整和大量優(yōu)化昧旨。1.4.x及之前版本,SQL改寫是在SQL路由之前完成的祥得,在1.5.x中調整為SQL路由之后兔沃,因為SQL改寫可以根據(jù)路由至單庫表還是多庫表而進行進一步優(yōu)化。SQL改寫分為正確性改寫和優(yōu)化改寫兩部分级及。

正確性改寫包括將分表的邏輯表名稱替換為真實表名稱乒疏,修正分頁信息和增加補列。舉兩個例子:

AVG計算饮焦。分布式場景怕吴,以avg1 + avg2 + avg3 / 3計算平均值并不正確窍侧,需要改寫為 (sum1 + sum2 + sum3) / (count1 + count2 + count3)。這就需要將包含AVG的SQL改寫為SUM和COUNT转绷,并在結果歸并時重新計算平均值伟件。

分頁。假設每10條數(shù)據(jù)為一頁议经,取第2頁數(shù)據(jù)斧账。在分片環(huán)境下獲取LIMIT 10, 10,歸并之后再根據(jù)排序條件取出前10條數(shù)據(jù)是不正確的結果煞肾。正確的做法是將分條件改寫為LIMIT 0, 20咧织,取出所有前兩頁數(shù)據(jù),再結合排序條件計算出正確的數(shù)據(jù)籍救。因此越是獲取靠后數(shù)據(jù)习绢,分頁的效率就會越低。有很多方法可避免使用LIMIT進行分頁钧忽。比如構建記錄行記錄數(shù)和行偏移量的二級索引毯炮,或使用上次分頁數(shù)據(jù)結尾ID作為下次查詢條件的分頁方式。

優(yōu)化改寫是1.5.x重點提升的部分耸黑,實現(xiàn)的功能比較零散桃煎,這里同樣舉兩個例子:

單路由拒絕改寫。這是將SQL改寫挪到SQL路由之后的原因大刊。當獲得路由結果之后为迈,單路由的情況因為不涉及到結果歸并,因此分頁缺菌、補列等改寫都無需存在葫辐。尤其是分頁,無需將數(shù)據(jù)從第1條開始取伴郁,節(jié)省了網絡帶寬耿战。

流式歸并改寫。一會講到歸并時會說焊傅,這里先提一句剂陡,將僅包含GROUPBY的SQL改寫為GROUPBY + ORDERBY。

SQL執(zhí)行

路由至真實數(shù)據(jù)源后狐胎,Sharding -JDBC將采用多線程并發(fā)執(zhí)行SQL鸭栖。它用3種執(zhí)行引擎分別對應處理Statement,PreparedStatement和AddBatchPreparedStatement握巢。Sharding-JDBC線程池放在一個名為ShardingContext的對象中晕鹊,它的生命周期同ShardingDataSource保持一致。如果一個應用中創(chuàng)建了多個Sharding-JDBC的數(shù)據(jù)源,它們將持有不同的線程池溅话。

結果歸并

Sharding-JDBC支持的結果歸并從功能上分為遍歷晓锻、排序、分組和分頁4種類型飞几,它們是組合而非互斥的關系带射。從結構劃分,可分為流式歸并循狰、內存歸并和裝飾者歸并。流式歸并和內存歸并是互斥的券勺,裝飾者歸并可以在流式歸并和內存歸并之上做進一步的處理绪钥。

流式歸并是將數(shù)據(jù)游標與結果集的游標保持一致,順序的從結果集中一條條的獲取正確的數(shù)據(jù)关炼。遍歷和排序都是流式歸并程腹,分組比較復雜,分為流式分組和內存分組儒拂。內存歸并則是需要將結果集的所有數(shù)據(jù)都遍歷并存儲在內存中寸潦,再通過內存歸并后,將內存中的數(shù)據(jù)偽裝成結果集返回社痛。

遍歷類型最為簡單见转,只需將多結果集組成鏈表,遍歷完成當前結果集后蒜哀,將鏈表位置后移斩箫,繼續(xù)遍歷下一個結果集即可。

排序類型稍微復雜撵儿,由于ORDER BY的原因乘客,每個結果集自身數(shù)據(jù)是有序的,因此只需要將結果集當前游標指向的值排序即可淀歇。Sharding-JDBC在排序類型歸并時易核,將每個結果集的當前排序數(shù)據(jù)實現(xiàn)了比較器,并將其放入優(yōu)先級隊列浪默。每次JDBC調用next時牡直,將隊列頂端的結果集出隊并next,然后獲取新的隊列頂端的結果集供JDBC獲取數(shù)據(jù)浴鸿。

分組類型最為復雜井氢,分組歸并已經不屬于OLTP范疇,而更面向OLAP岳链,但由于遺留系統(tǒng)使用很多花竞,因此Sharding-JDBC還是將其實現(xiàn)。分組歸并分成流式分組歸并和內存分組歸并。流式分組歸并節(jié)省內存约急,但必須要求排序和分組的數(shù)據(jù)保持一致零远。如果GROUPBY和ORDER BY的內容不一致,則必須使用內存分組歸并厌蔽。由于數(shù)據(jù)不是按照分組需要的順序取出牵辣,因此需要將結果集中的所有數(shù)據(jù)全部加載至內存。在SQL改寫時提到的僅有GROUP BY的SQL奴饮,會優(yōu)化增加ORDER BY語句纬向,即使將內存分組歸并優(yōu)化為流式分組歸并的提升。

無論是流式分組還是內存分組戴卜,對聚合的處理都是一致的逾条。聚合分為比較、累加和平均值3種類型投剥。比較聚合包括MAX和MIN师脂,只返回最大(小)結果江锨。累加聚合包括SUM和COUNT吃警,需要將結果累加后返回。平均值聚合則是通過SQL改寫的SUM和COUNT計算啄育,相關內容已在SQL改寫涵蓋酌心,不再贅述。

最后再聊一下裝飾者歸并灸撰,他是對所有的結果集歸并進行統(tǒng)一的功能增強谒府,目前裝飾者歸并只有分頁一種類型。

上述的所有歸并類型浮毯,都可能分頁或不分頁完疫,因此可以通過裝飾者模式來增加分頁的能力。分頁歸并會將改寫的LIMIT中债蓝,不需要獲取的數(shù)據(jù)過濾掉壳鹤。Sharding-JDBC的分頁很容易產生誤解,很多人認為分頁會占用大量內存饰迹,因為Sharding-JDBC會因為分布式正確性的考量芳誓,將LIMIT 100000, 10改寫為LIMIT 0, 100010,產生Sharding-JDBC會將100010數(shù)據(jù)都加載到內存的錯覺啊鸭。通過上面分析可知锹淌,會全部加載到內存的只有內存分組歸并這一種情況。其他情況都是通過流式獲取結果集數(shù)據(jù)的方式赠制,因此Sharding-JDBC會通過結果集的next方法將無需取出的數(shù)據(jù)全部跳過赂摆,并不會將其存入內存。

分布式主鍵

分布式主鍵在這里單獨提煉出一個章節(jié),因為它是貫穿于Sharding-JDBC整個生命周期的烟号。

分布式主鍵最獨立的部分是生成策略绊谭,Sharding-JDBC提供靈活的配置分布式主鍵生成策略方式。在分片規(guī)則配置模塊可配置每個表的主鍵生成策略汪拥,默認使用snowflake达传。

通過策略生成的分布式主鍵可以無縫的融入JDBC協(xié)議,它實現(xiàn)了Statement的getGeneratedKeys方法迫筑,將其返回改寫后的Result和ResultMetaData宪赶,將Sharding-JDBC生成的分布式主鍵偽裝為數(shù)據(jù)庫生成的自增主鍵返回。

SQL解析時脯燃,需要根據(jù)分布式主鍵配置策略判斷是否在邏輯SQL中已包含主鍵列逊朽,如果未包含則需要將INSERTItems和INSERT Values的最后位置寫入解析上下文。

SQL改寫時曲伊,將根據(jù)解析上下文中的位置改寫SQL,增加未包含的主鍵列名稱和值追他。如果是Statement則在INSERT Values后追加生成后的分布式主鍵坟募;如果是PreparedStatement則在INSERT Values后追加?邑狸,并在傳入的參數(shù)后追加生成后的分布式主鍵懈糯。

更新SQL解析比查詢SQL解析復雜度低的多的多。不同數(shù)據(jù)庫在插入SQL語法上也統(tǒng)一的多单雾。本文分享 MySQL 更新SQL解析器 MySQLUpdateParser赚哗。

MySQL UPDATE 語法一共有 2 種 :

第一種:Single-table syntax

UPDATE [LOW_PRIORITY] [IGNORE] table_reference

SET col_name1={expr1|DEFAULT} [, col_name2={expr2|DEFAULT}] ...

[WHERE where_condition]

[ORDER BY ...]

[LIMIT row_count]

第二種:Multiple-table syntax

UPDATE [LOW_PRIORITY] [IGNORE] table_references

SET col_name1={expr1|DEFAULT} [, col_name2={expr2|DEFAULT}] ...

[WHERE where_condition]

Sharding-JDBC 目前僅支持第一種。業(yè)務場景上使用第二種的很少很少硅堆。

Sharding-JDBC 更新SQL解析主流程如下:

// AbstractUpdateParser.java

@Override

public UpdateStatement parse() {

sqlParser.getLexer().nextToken(); // 跳過 UPDATE

skipBetweenUpdateAndTable(); // 跳過關鍵字屿储,例如:MYSQL 里的 LOW_PRIORITY、IGNORE

sqlParser.parseSingleTable(updateStatement); // 解析表

parseSetItems(); // 解析 SET

sqlParser.skipUntil(DefaultKeyword.WHERE);

sqlParser.setParametersIndex(parametersIndex);

sqlParser.parseWhere(updateStatement);

return updateStatement; // 解析 WHERE

}

Sharding-JDBC 正在收集使用公司名單:傳送門渐逃。

你的登記够掠,會讓更多人參與和使用 Sharding-JDBC。傳送門

Sharding-JDBC 也會因此茄菊,能夠覆蓋更多的業(yè)務場景疯潭。傳送門

登記吧,騷年面殖!傳送門

2. UpdateStatement

更新SQL 解析結果竖哩。

public final class UpdateStatement extends AbstractSQLStatement {

}

對过咬,沒有其他屬性恢着。

我們來看下 UPDATE t_user SET nickname = ?, age = ? WHERE user_id = ? 的解析結果

3. #parse()

3.1 #skipBetweenUpdateAndTable()

在 UPDATE 和 表名 之間有些詞法株婴,對 SQL 路由和改寫無影響,進行跳過较曼。

// MySQLUpdateParser.java

@Override

protected void skipBetweenUpdateAndTable() {

getSqlParser().skipAll(MySQLKeyword.LOW_PRIORITY, MySQLKeyword.IGNORE);

}

// OracleUpdateParser.java

@Override

protected void skipBetweenUpdateAndTable() {

getSqlParser().skipIfEqual(OracleKeyword.ONLY);

}

3.2 #parseSingleTable()

解析,請看《SQL 解析(二)之SQL解析》的 #parseSingleTable() 小節(jié)遣鼓。

3.3 #parseSetItems()

解析SET后語句滨溉。

// AbstractUpdateParser.java

/**

* 解析多個 SET 項

*/

private void parseSetItems() {

sqlParser.accept(DefaultKeyword.SET);

do {

parseSetItem();

} while (sqlParser.skipIfEqual(Symbol.COMMA)); // 以 "," 分隔

}

/**

* 解析單個 SET 項

*/

private void parseSetItem() {

parseSetColumn();

sqlParser.skipIfEqual(Symbol.EQ, Symbol.COLON_EQ);

parseSetValue();

}

/**

* 解析單個 SET 項

*/

private void parseSetColumn() {

if (sqlParser.equalAny(Symbol.LEFT_PAREN)) {

sqlParser.skipParentheses();

return;

}

int beginPosition = sqlParser.getLexer().getCurrentToken().getEndPosition();

String literals = sqlParser.getLexer().getCurrentToken().getLiterals();

sqlParser.getLexer().nextToken();

if (sqlParser.skipIfEqual(Symbol.DOT)) { // 字段有別名

// TableToken

if (updateStatement.getTables().getSingleTableName().equalsIgnoreCase(SQLUtil.getExactlyValue(literals))) {

updateStatement.getSqlTokens().add(new TableToken(beginPosition - literals.length(), literals));

}

sqlParser.getLexer().nextToken();

}

}

/**

* 解析單個 SET 值

*/

private void parseSetValue() {

sqlParser.parseExpression(updateStatement);

parametersIndex = sqlParser.getParametersIndex();

}

3.4 #parseWhere()

Sharding-JDBC將作為面向OLTP在線業(yè)務的分片化的數(shù)據(jù)庫治理微服務基礎組件積極的發(fā)展下去。真誠邀請感興趣的人關注和參與埠通。

在此我向大家推薦一個架構學習交流群赎离。交流學習群號:938837867 暗號:555 里面會分享一些資深架構師錄制的視頻錄像:有Spring,MyBatis端辱,Netty源碼分析梁剔,高并發(fā)、高性能舞蔽、分布式荣病、微服務架構的原理,JVM性能優(yōu)化渗柿、分布式架構等這些成為架構師必備

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末个盆,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子朵栖,更是在濱河造成了極大的恐慌颊亮,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件陨溅,死亡現(xiàn)場離奇詭異终惑,居然都是意外死亡,警方通過查閱死者的電腦和手機门扇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進店門雹有,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人臼寄,你說我怎么就攤上這事霸奕。” “怎么了吉拳?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵铅祸,是天一觀的道長。 經常有香客問我合武,道長临梗,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任稼跳,我火速辦了婚禮盟庞,結果婚禮上,老公的妹妹穿的比我還像新娘汤善。我一直安慰自己什猖,他們只是感情好票彪,可當我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著不狮,像睡著了一般降铸。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上摇零,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天推掸,我揣著相機與錄音,去河邊找鬼驻仅。 笑死谅畅,一個胖子當著我的面吹牛,可吹牛的內容都是我干的噪服。 我是一名探鬼主播毡泻,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼粘优!你這毒婦竟也來了仇味?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤雹顺,失蹤者是張志新(化名)和其女友劉穎邪铲,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體无拗,經...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年昧碉,在試婚紗的時候發(fā)現(xiàn)自己被綠了英染。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡被饿,死狀恐怖四康,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情狭握,我是刑警寧澤闪金,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站论颅,受9級特大地震影響哎垦,放射性物質發(fā)生泄漏。R本人自食惡果不足惜恃疯,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一漏设、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧今妄,春花似錦郑口、人聲如沸鸳碧。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽瞻离。三九已至,卻和暖如春乒裆,著一層夾襖步出監(jiān)牢的瞬間套利,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工缸兔, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留日裙,地道東北人。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓惰蜜,卻偏偏與公主長得像昂拂,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子抛猖,可洞房花燭夜當晚...
    茶點故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內容