《輕量級數據庫中間件利器Sharding-JDBC深度解析》要點:
本文介紹了輕量級數據庫中間件利器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完整的實現了分庫分表,讀寫分離和分布式主鍵功能,并初步實現了柔性事務.
它直接實現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和原生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.在Sharding-JDBC 1.5.x版本,SQL改寫進行了調整和大量優化.1.4.x及之前版本,SQL改寫是在SQL路由之前完成的,在1.5.x中調整為SQL路由之后,因為SQL改寫可以根據路由至單庫表還是多庫表而進行進一步優化.SQL改寫分為正確性改寫和優化改寫兩部分.
正確性改寫包括將分表的邏輯表名稱替換為真實表名稱,修正分頁信息和增加補列.舉兩個例子:
優化改寫是1.5.x重點提升的部分,實現的功能比較零散,這里同樣舉兩個例子:
路由至真實數據源后,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每個里程碑版本的歷程.
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社群
轉載請注明本頁網址:
http://www.snjht.com/jiaocheng/2198.html