1. 程式人生 > >輕量級資料庫中介軟體利器Sharding-JDBC深度解析

輕量級資料庫中介軟體利器Sharding-JDBC深度解析

講師介紹

張亮

噹噹架構部總監

  • 負責分散式中介軟體和私有云平臺建設
  • 目前主導開源專案:Elastic-Job及Sharding-JDBC

主題簡介:

1、關係型資料庫中介軟體核心功能介紹

2、Sharding-JDBC架構及核心解析

3、Sharding-JDBC未來展望

一、關係型資料庫中介軟體核心功能介紹

關係型資料庫憑藉靈活查詢的SQL和穩定的儲存及事務引擎,一直以來是業務儲存領域的首選。而在規模越來越大的網際網路年代,單一的關係型資料庫卻已難滿足需求。開發人員不願放棄SQL查詢的靈活度及對之前程式碼的相容性,而又無法承受資料量過大時所帶來的效能瓶頸。因此NoSQL和NewSQL分別產生,而NoSQL的不相容性和NewSQL的不成熟,也使得關係型資料庫仍將長期存在下去。所以,繼續使用關係型資料庫,並能解決網際網路規模所帶來的衝擊,資料庫中間層這個既不保守,也不激進的平衡方案被大多數網際網路公司所接受。關係型資料庫中介軟體採用單體向分散式透明轉化的方案來解決資料量和訪問量巨大這兩個網際網路場景的核心問題。

關係型資料庫在大於自身資料量閥值的情況下,效能會急劇下降。在面對網際網路海量資料情況時,所有資料都存於單表,顯然會輕易超過資料庫表可承受的範圍。這個單表可承受的資料量閥值,需根據資料庫和併發量的差異,通過實際測試獲得。

通過分庫和分表拆分資料來使得各個表的資料量保持在閥值以下。拆分方式為垂直拆分和水平拆分。垂直拆分是根據業務將單庫(表)拆分為多庫(表)。如:將常用的欄位和不常用的欄位拆分至不同的庫(表)中。但垂直拆分需要對架構和設計進行調整,往往來不及應對網際網路業務需求的快速變化,而且也並不能真正的解決單點瓶頸。水平拆分則是根據分片演算法將一個庫(表)拆分為多個庫(表)。如:根據ID的最後一位以10取餘,尾數是0的放入0庫(表),尾數是1的放入1庫(表)。水平拆分從理論上突破了單機資料量處理的瓶頸,並且擴充套件相對自由,是分庫分表的標準解決方案。

分庫和讀寫分離疏導流量是應對高訪問量的常見手段。分表雖然可以解決海量資料導致的效能問題,但無法解決過多請求訪問同一資料庫,導致其響應變慢的問題。所以水平拆分通常要採取分庫的方式,一併解決資料量和訪問量巨大的問題。讀寫分離是另一個疏導流量的辦法,但讀寫資料間的延遲是架構設計時需要考慮的問題。

雖然分庫可以解決上述問題,但分散式架構在獲得了收益的同時,也帶來了新的問題。

跨庫事務是分散式資料庫要面對的棘手事情。合理採用分表,可以在降低單表資料量的情況下,儘量使用本地事務,善於使用同庫不同表可有效避免分散式事務帶來的麻煩。在不能避免跨庫事務的場景,有些業務仍然需要保持事務的一致性。而基於XA的分散式事務由於效能低下,無法被網際網路公司所採納,它們大多采用最終一致性的柔性事務代替分散式事務。

因此,最佳實踐是合理地配合使用分庫+分表。在實現上,分表的難度遠大於分庫,它需要對SQL解析,並且對錶名改寫,而分庫則不需要。

另一個分散式衍生的問題是主鍵生成。它必須可以保證分散式唯一。分散式主鍵的生成方式分為中心化和去中心化兩大類。中心化可以繼續採用資料庫生成自增主鍵的方式,為每個不同的分庫設定不同的初始值,並將步長設定為分片的個數即可,這種方式對分片個數有依賴,一旦再次水平擴充套件,原有的分散式主鍵不易遷移。還有一種中心化生成分散式主鍵的方式,即採用Redis在記憶體中生成自增序列,但此種方式新增加了一個外部元件的依賴,一旦Redis不可用,則整個資料庫將無法在插入,可用性會大大下降,另外Redis的單點問題也需要解決,部署複雜度較高。去中心化方式無需額外部署,可擴充套件性也很好,因此更推薦使用。UUID是去中心化生成分散式主鍵較為常見的一種方式,但它的主鍵很長,而且無序,通過主鍵排序時對資料庫的效能影響較大,不建議使用。目前較為完美的方案是使用snowflake演算法生成分散式唯一和基本有序的主鍵。

綜上所述,分散式資料庫中介軟體的功能模組非常清晰,有其存在的必要和市場價值。但與旺盛的需求形成鮮明對比,成熟的關係型資料庫中介軟體鳳毛麟角。主要原因是各公司均開發各自的中介軟體,沒有形成統一的標準及規範,也沒有動力從公司現有的系統中解耦並獨立開源。噹噹同樣自研,並決定將其開源,命名為Sharding-JDBC。從2016年開源至今,已釋出了15個版本,其中包含5個里程碑版本升級。在經歷了整體架構的數次精煉以及穩定性打磨後,如今它已積累了足夠的底蘊,相信可以成為開發者選擇技術元件時的一個參考。

二、Sharding-JDBC架構及核心解析

Sharding-JDBC完整的實現了分庫分表,讀寫分離和分散式主鍵功能,並初步實現了柔性事務。

它直接實現JDBC介面,舊程式碼遷移成本幾乎為零,可適用於任何基於Java的ORM框架,如:JPA、Hibernate、Mybatis、SpringJDBC Template等;理論上可以支援所有實現JDBC協議的資料庫,但由於各種資料庫的SQL方言差別較大,每種SQL都需要獨立的解析器,Sharding-JDBC目前僅支援MySQL、PostgreSQL、Oracle和SQL Server這4種最主流的資料庫。由於柔性事務與JDBC沒有直接關係,因此正在考慮將它拆分為一個獨立的專案。

Sharding-JDBC與基於MySQL等資料庫協議實現的Proxy中間層在部署架構上差別很大,但在程式碼的核心邏輯上差別並不大。Sharding-JDBC作為lib庫,是與業務程式碼部署在一起的,而基於Proxy的中間層則是架在資料庫的前方,與應用程式碼在部署上隔絕。無論使用哪種架構,它們的核心邏輯均極為相似,都會分為分片規則配置、SQL解析、SQL路由、SQL改寫、SQL執行以及結果歸併模組,區別僅在於協議實現層的不同(JDBC或資料庫協議)。

Sharding-JDBC架構圖如下:

架構

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

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

在資料量一致的情況下,使用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在查詢中的效能損失非常低,插入和更新略高。

將單表的資料拆分為二,放入兩個表中,使用Sharding-JDBC和原生JDBC的效能測試報告如下:

  • 查詢操作:TPS雙庫比單庫可以增加大約94%的效能。
  • 插入操作:TPS雙庫比單庫可以增加大約60%的效能。
  • 更新操作:TPS雙庫比單庫可以增加大約89%的效能。
  • 結果表明,Sharding-JDBC可有效利用水平擴充套件大幅度提升效能。

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

分片規則配置

Sharding-JDBC的分片策略配置是自定義的,因此可以通過程式設計的方式最大限度的靈活調整。它並不僅支援=運算子分片,可支援BETWEEN和IN的運算子分片,支援將一條邏輯SQL最終散落至多個數據節點。同時支援多分片鍵,例如:根據使用者ID分庫,訂單ID分表這種分庫分表結合的分片策略;或根據年分庫,月份+使用者區域ID分表這樣的多片鍵分片。

通過程式設計的方式定製分片規則雖然靈活,但配置起來略顯繁瑣。因此Sharding-JDBC又提供了Inline表示式編寫分片策略的方式,用於配置集中化,以避免配置散落在配置檔案和程式碼中的情況。此外,它還提供了定製化的Spring名稱空間和YAML進一步簡化配置。

JDBC規範重寫

Sharding-JDBC對JDBC規範的重寫思路是針對DataSource、Connection、Statement、PreparedStatement和ResultSet這5個核心介面封裝,將多個實現類集合納入Sharding-JDBC實現類管理。分散式主鍵也屬於JDBC協議的一部分。

Sharding-JDBC儘量最大化實現JDBC協議,但分散式畢竟與原生JDBC不同,所以目前仍有未實現的介面,包括遊標,儲存過程、SavePoint以及向前遍歷和修改ResultSet等不太常用的功能。此外,為了保證相容性,並未實現JDBC 4.1及其後釋出的介面(如: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的依次解析,因此解析效能很高。由於各種資料庫的SQL差異很大,因此在解析模組對每種資料庫提供方言的支援。

Sharding-JDBC支援各種連線、聚合、排序、分組以及分頁的解析,並且可以有限度的支援子查詢。

SQL路由

SQL路由是根據分片規則配置以及解析上下文中的分片條件,將SQL定位至真正的資料來源。它又分為直接路由、簡單路由和笛卡爾積路由。

滿足直接路由的條件比較苛刻,如果通過Hint(通過HintAPI直接指定路由至庫表)方式分片,且僅分庫,則無需SQL解析和結果歸併。因此它的SQL相容性最好,可以執行包括子查詢、OR、UNION等複雜情況的任意SQL。

簡單路由是Sharding-JDBC最推薦使用的分片方式,它是指不包含JOIN或僅包含Binding表JOIN的SQL。Binding表是指使用同樣的分片鍵和分片規則的一組表,也就是說任何情況下,Binding表的分片結果應與主表一致。例如:order表和order_item表,都根據order_id分片,結果應是order_1與order_item_1成對出現。這樣的關聯查詢和單表查詢複雜度和效能相當。如果分片條件不是等於,而是BETWEEN或IN,則路由結果不一定落入單庫(表),因此一條邏輯SQL最終可能拆分為多條SQL語句。

笛卡爾積查詢最為複雜,因為無法根據Binding關係定位分片規則的一致性,所以非Binding表的關聯查詢需要拆解為笛卡爾積組合執行。查詢效能較低,而且資料庫連線數較高,需謹慎使用。

SQL改寫

SQL改寫模組的用途是將邏輯SQL改寫為可以分散式執行的SQL。在Sharding-JDBC 1.5.x版本,SQL改寫進行了調整和大量優化。1.4.x及之前版本,SQL改寫是在SQL路由之前完成的,在1.5.x中調整為SQL路由之後,因為SQL改寫可以根據路由至單庫表還是多庫表而進行進一步優化。SQL改寫分為正確性改寫和優化改寫兩部分。

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

  • AVG計算。分散式場景,以avg1 + avg2 + avg3 / 3計算平均值並不正確,需要改寫為 (sum1 + sum2 + sum3)  / (count1 + count2 + count3)。這就需要將包含AVG的SQL改寫為SUM和COUNT,並在結果歸併時重新計算平均值。
  • 分頁。假設每10條資料為一頁,取第2頁資料。在分片環境下獲取LIMIT 10, 10,歸併之後再根據排序條件取出前10條資料是不正確的結果。正確的做法是將分條件改寫為LIMIT 0, 20,取出所有前兩頁資料,再結合排序條件計算出正確的資料。因此越是獲取靠後資料,分頁的效率就會越低。有很多方法可避免使用LIMIT進行分頁。比如構建記錄行記錄數和行偏移量的二級索引,或使用上次分頁資料結尾ID作為下次查詢條件的分頁方式。

優化改寫是1.5.x重點提升的部分,實現的功能比較零散,這裡同樣舉兩個例子:

  • 單路由拒絕改寫。這是將SQL改寫挪到SQL路由之後的原因。當獲得路由結果之後,單路由的情況因為不涉及到結果歸併,因此分頁、補列等改寫都無需存在。尤其是分頁,無需將資料從第1條開始取,節省了網路頻寬。
  • 流式歸併改寫。一會講到歸併時會說,這裡先提一句,將僅包含GROUPBY的SQL改寫為GROUPBY + ORDERBY。
SQL執行

路由至真實資料來源後,Sharding -JDBC將採用多執行緒併發執行SQL。它用3種執行引擎分別對應處理Statement,PreparedStatement和AddBatchPreparedStatement。Sharding-JDBC執行緒池放在一個名為ShardingContext的物件中,它的生命週期同ShardingDataSource保持一致。如果一個應用中建立了多個Sharding-JDBC的資料來源,它們將持有不同的執行緒池。

結果歸併

Sharding-JDBC支援的結果歸併從功能上分為遍歷、排序、分組和分頁4種類型,它們是組合而非互斥的關係。從結構劃分,可分為流式歸併、記憶體歸併和裝飾者歸併。流式歸併和記憶體歸併是互斥的,裝飾者歸併可以在流式歸併和記憶體歸併之上做進一步的處理。

流式歸併是將資料遊標與結果集的遊標保持一致,順序的從結果集中一條條的獲取正確的資料。遍歷和排序都是流式歸併,分組比較複雜,分為流式分組和記憶體分組。記憶體歸併則是需要將結果集的所有資料都遍歷並存儲在記憶體中,再通過記憶體歸併後,將記憶體中的資料偽裝成結果集返回。

遍歷型別最為簡單,只需將多結果集組成連結串列,遍歷完成當前結果集後,將連結串列位置後移,繼續遍歷下一個結果集即可。

排序型別稍微複雜,由於ORDER BY的原因,每個結果集自身資料是有序的,因此只需要將結果集當前遊標指向的值排序即可。Sharding-JDBC在排序型別歸併時,將每個結果集的當前排序資料實現了比較器,並將其放入優先順序佇列。每次JDBC呼叫next時,將佇列頂端的結果集出隊並next,然後獲取新的佇列頂端的結果集供JDBC獲取資料。

分組型別最為複雜,分組歸併已經不屬於OLTP範疇,而更面向OLAP,但由於遺留系統使用很多,因此Sharding-JDBC還是將其實現。分組歸併分成流式分組歸併和記憶體分組歸併。流式分組歸併節省記憶體,但必須要求排序和分組的資料保持一致。如果GROUPBY和ORDER BY的內容不一致,則必須使用記憶體分組歸併。由於資料不是按照分組需要的順序取出,因此需要將結果集中的所有資料全部載入至記憶體。在SQL改寫時提到的僅有GROUP BY的SQL,會優化增加ORDER BY語句,即使將記憶體分組歸併優化為流式分組歸併的提升。

無論是流式分組還是記憶體分組,對聚合的處理都是一致的。聚合分為比較、累加和平均值3種類型。比較聚合包括MAX和MIN,只返回最大(小)結果。累加聚合包括SUM和COUNT,需要將結果累加後返回。平均值聚合則是通過SQL改寫的SUM和COUNT計算,相關內容已在SQL改寫涵蓋,不再贅述。

最後再聊一下裝飾者歸併,他是對所有的結果集歸併進行統一的功能增強,目前裝飾者歸併只有分頁一種型別。

上述的所有歸併型別,都可能分頁或不分頁,因此可以通過裝飾者模式來增加分頁的能力。分頁歸併會將改寫的LIMIT中,不需要獲取的資料過濾掉。Sharding-JDBC的分頁很容易產生誤解,很多人認為分頁會佔用大量記憶體,因為Sharding-JDBC會因為分散式正確性的考量,將LIMIT 100000, 10改寫為LIMIT 0, 100010,產生Sharding-JDBC會將100010資料都載入到記憶體的錯覺。通過上面分析可知,會全部載入到記憶體的只有記憶體分組歸併這一種情況。其他情況都是通過流式獲取結果集資料的方式,因此Sharding-JDBC會通過結果集的next方法將無需取出的資料全部跳過,並不會將其存入記憶體。

分散式主鍵

分散式主鍵在這裡單獨提煉出一個章節,因為它是貫穿於Sharding-JDBC整個生命週期的。

分散式主鍵最獨立的部分是生成策略,Sharding-JDBC提供靈活的配置分散式主鍵生成策略方式。在分片規則配置模組可配置每個表的主鍵生成策略,預設使用snowflake。

通過策略生成的分散式主鍵可以無縫的融入JDBC協議,它實現了Statement的getGeneratedKeys方法,將其返回改寫後的Result和ResultMetaData,將Sharding-JDBC生成的分散式主鍵偽裝為資料庫生成的自增主鍵返回。

SQL解析時,需要根據分散式主鍵配置策略判斷是否在邏輯SQL中已包含主鍵列,如果未包含則需要將INSERTItems和INSERT Values的最後位置寫入解析上下文。

SQL改寫時,將根據解析上下文中的位置改寫SQL,增加未包含的主鍵列名稱和值。如果是Statement則在INSERT Values後追加生成後的分散式主鍵;如果是PreparedStatement則在INSERT Values後追加?,並在傳入的引數後追加生成後的分散式主鍵。

受限於篇幅,讀寫分離、柔性事務就不在此說明了。

三、Sharding-JDBC未來展望

首先,請和我一同回顧下Sharding-JDBC每個里程碑版本的歷程。

1.0.x:分庫分表

1.1.x:配置簡易化

1.2.x:柔性事務

1.3.x:讀寫分離

1.4.x:分散式主鍵

1.5.x:自研解析引擎 + 多資料庫支援

通過這5個版本的迭代可以看到,Sharding-JDBC的精力主要集中在透明化分散式資料庫這部分,因此經常有人問Sharding-JDBC和基於Proxy的資料庫中間層有什麼區別?和NewSQL資料庫又有什麼區別?

儘管部署架構不同,但功能上確實差異不明顯。不過結構的不同終會將它們推向不同的方向。Sharding-JDBC與業務程式碼部署在一起的架構,非常適合作為微服務的資料訪問層基礎開發元件。Proxy和NewSQL是面向運維的資料庫,而Sharding-JDBC的定位與噹噹一併開源的DubboX、Elastic-Job一樣,是面向開發的微服務基礎類庫,它始終以雲原生的基礎開發套件為目標。

Sharding-JDBC 1.6.x到來,將會愈加明顯的劃清界限。Sharding-JDBC 1.6.x的目標是配置動態化和資料庫治理,通過將配置存入註冊中心,達到治理分庫分表+讀寫分離的資料庫的目的。在應用端進行資料庫發現、流量疏導、故障轉移、熔斷等功能,向治理服務一樣治理資料庫。

Sharding-JDBC將作為面向OLTP線上業務的分片化的資料庫治理微服務基礎元件積極的發展下去。真誠邀請感興趣的人關注和參與。

點選文末【閱讀原文】或登入https://github.com/dangdangdotcom/sharding-jdbc 即可進入Sharding-JDBC開源地址。

直播連結

回聽直播請戳:https://m.qlchat.com/topic/details?topicId=260000426036664&isGuide=Y

密碼:123

文章來自微信公眾號:DBAplus社群