基本概念
在了解Sharding-JDBC的執行原理前,需要了解以下概念:
邏輯表
水平拆分的數據表的總稱。例:訂單數據表根據主鍵尾數拆分為10張表,分別是 t_order_0 、 t_order_1 到t_order_9 ,他們的邏輯表名為 t_order 。
真實表
在分片的數據庫中真實存在的物理表。即上個示例中的 t_order_0 到 t_order_9 。
數據節點
數據分片的最小物理單元。由數據源名稱和數據表組成,例: ds_0.t_order_0 。
綁定表
指分片規則一致的主表和子表。例如: t_order 表和 t_order_item 表,均按照 order_id 分片,綁定表之間的分區
鍵完全相同,則此兩張表互為綁定表關系。綁定表之間的多表關聯查詢不會出現笛卡爾積關聯,關聯查詢效率將大
大提升。舉例說明,如果SQL為:
SELECT i.* FROM t_order o JOIN t_order_item i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
在不配置綁定表關系時,假設分片鍵 order_id 將數值10路由至第0片,將數值11路由至第1片,那么路由后的SQL
應該為4條,它們呈現為笛卡爾積:
SELECT i.* FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11); SELECT i.* FROM t_order_0 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11); SELECT i.* FROM t_order_1 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11); SELECT i.* FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
在配置綁定表關系后,路由的SQL應該為2條:
SELECT i.* FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11); SELECT i.* FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
廣播表
指所有的分片數據源中都存在的表,表結構和表中的數據在每個數據庫中均完全一致。適用於數據量不大且需要與
海量數據的表進行關聯查詢的場景,例如:字典表。
分片鍵
用於分片的數據庫字段,是將數據庫(表)水平拆分的關鍵字段。例:將訂單表中的訂單主鍵的尾數取模分片,則訂
單主鍵為分片字段。 SQL中如果無分片字段,將執行全路由,性能較差。 除了對單分片字段的支持,ShardingJdbc也支持根據多個字段進行分片。
分片算法
通過分片算法將數據分片,支持通過 = 、 BETWEEN 和 IN 分片。分片算法需要應用方開發者自行實現,可實現的靈
活度非常高。包括:精確分片算法 、范圍分片算法 ,復合分片算法 等。例如:where order_id = ? 將采用精確分
片算法,where order_id in (?,?,?)將采用精確分片算法,where order_id BETWEEN ? and ? 將采用范圍分片算
法,復合分片算法用於分片鍵有多個復雜情況。
分片策略
包含分片鍵和分片算法,由於分片算法的獨立性,將其獨立抽離。真正可用於分片操作的是分片鍵 + 分片算法,也
就是分片策略。內置的分片策略大致可分為尾數取模、哈希、范圍、標簽、時間等。由用戶方配置的分片策略則更
加靈活,常用的使用行表達式配置分片策略,它采用Groovy表達式表示,如: t_user_$->{u_id % 8} 表示t_user
表根據u_id模8,而分成8張表,表名稱為 t_user_0 到 t_user_7 。
自增主鍵生成策略
通過在客戶端生成自增主鍵替換以數據庫原生自增主鍵的方式,做到分布式主鍵無重復。
.SQL解析
當Sharding-JDBC接受到一條SQL語句時,會陸續執行 SQL解析 => 查詢優化 => SQL路由 => SQL改寫 => SQL執行 =>
結果歸並 ,最終返回執行結果。
SQL解析過程分為詞法解析和語法解析。 詞法解析器用於將SQL拆解為不可再分的原子符號,稱為Token。並根據
不同數據庫方言所提供的字典,將其歸類為關鍵字,表達式,字面量和操作符。 再使用語法解析器將SQL轉換為抽
象語法樹。
例如,以下SQL:
SELECT id, name FROM t_user WHERE status = 'ACTIVE' AND age > 18
解析之后的為抽象語法樹見下圖:
為了便於理解,抽象語法樹中的關鍵字的Token用綠色表示,變量的Token用紅色表示,灰色表示需要進一步拆分。
最后,通過對抽象語法樹的遍歷去提煉分片所需的上下文,並標記有可能需要SQL改寫(后邊介紹)的位置。 供分片
使用的解析上下文包含查詢選擇項(Select Items)、表信息(Table)、分片條件(Sharding Condition)、自增
主鍵信息(Auto increment Primary Key)、排序信息(Order By)、分組信息(Group By)以及分頁信息
(Limit、Rownum、Top)。
SQL路由
SQL路由就是把針對邏輯表的數據操作映射到對數據結點操作的過程。
根據解析上下文匹配數據庫和表的分片策略,並生成路由路徑。 對於攜帶分片鍵的SQL,根據分片鍵操作符不同可
以划分為單片路由(分片鍵的操作符是等號)、多片路由(分片鍵的操作符是IN)和范圍路由(分片鍵的操作符是
BETWEEN),不攜帶分片鍵的SQL則采用廣播路由。根據分片鍵進行路由的場景可分為直接路由、標准路由、笛卡爾路由等。
標准路由
標准路由是Sharding-Jdbc最為推薦使用的分片方式,它的適用范圍是不包含關聯查詢或僅包含綁定表之間關聯查
詢的SQL。 當分片運算符是等於號時,路由結果將落入單庫(表),當分片運算符是BETWEEN或IN時,則路由結
果不一定落入唯一的庫(表),因此一條邏輯SQL最終可能被拆分為多條用於執行的真實SQL。 舉例說明,如果按
照 order_id 的奇數和偶數進行數據分片,一個單表查詢的SQL如下:
SELECT * FROM t_order WHERE order_id IN (1, 2);
那么路由的結果應為:
SELECT * FROM t_order_0 WHERE order_id IN (1, 2); SELECT * FROM t_order_1 WHERE order_id IN (1, 2);
綁定表的關聯查詢與單表查詢復雜度和性能相當。舉例說明,如果一個包含綁定表的關聯查詢的 SQL如下:
SELECT * FROM t_order o JOIN t_order_item i ON o.order_id=i.order_id WHERE order_id IN (1, 2);
那么路由的結果應為:
SELECT * FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE order_id IN (1, 2); SELECT * FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE order_id IN (1, 2);
可以看到,SQL拆分的數目與單表是一致的。
笛卡爾路由
笛卡爾路由是最復雜的情況,它無法根據綁定表的關系定位分片規則,因此非綁定表之間的關聯查詢需要拆解為笛
卡爾積組合執行。 如果上個示例中的SQL並未配置綁定表關系,那么路由的結果應為:
SELECT * FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE order_id IN (1, 2); SELECT * FROM t_order_0 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE order_id IN (1, 2); SELECT * FROM t_order_1 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE order_id IN (1, 2); SELECT * FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE order_id IN (1, 2);
笛卡爾路由查詢性能較低,需謹慎使用。
全庫表路由
對於不攜帶分片鍵的SQL,則采取廣播路由的方式。根據SQL類型又可以划分為全庫表路由、全庫路由、全實例路
由、單播路由和阻斷路由這5種類型。其中全庫表路由用於處理對數據庫中與其邏輯表相關的所有真實表的操作,
主要包括不帶分片鍵的DQL(數據查詢)和DML(數據操縱),以及DDL(數據定義)等。例如:
SELECT * FROM t_order WHERE good_prority IN (1, 10);
則會遍歷所有數據庫中的所有表,逐一匹配邏輯表和真實表名,能夠匹配得上則執行。路由后成為
SELECT * FROM t_order_0 WHERE good_prority IN (1, 10); SELECT * FROM t_order_1 WHERE good_prority IN (1, 10); SELECT * FROM t_order_2 WHERE good_prority IN (1, 10); SELECT * FROM t_order_3 WHERE good_prority IN (1, 10);
SQL改寫
工程師面向邏輯表書寫的SQL,並不能夠直接在真實的數據庫中執行,SQL改寫用於將邏輯SQL改寫為在真實數據
庫中可以正確執行的SQL。
如一個簡單的例子,若邏輯SQL為:
SELECT order_id FROM t_order WHERE order_id=1;
假設該SQL配置分片鍵order_id,並且order_id=1的情況,將路由至分片表1。那么改寫之后的SQL應該為:
SELECT order_id FROM t_order_1 WHERE order_id=1;
再比如,Sharding-JDBC需要在結果歸並時獲取相應數據,但該數據並未能通過查詢的SQL返回。 這種情況主要是
針對GROUP BY和ORDER BY。結果歸並時,需要根據 GROUP BY 和 ORDER BY 的字段項進行分組和排序,但如果原
始SQL的選擇項中若並未包含分組項或排序項,則需要對原始SQL進行改寫。 先看一下原始SQL中帶有結果歸並所
需信息的場景:
SELECT order_id, user_id FROM t_order ORDER BY user_id;
由於使用user_id進行排序,在結果歸並中需要能夠獲取到user_id的數據,而上面的SQL是能夠獲取到user_id數據
的,因此無需補列。
如果選擇項中不包含結果歸並時所需的列,則需要進行補列,如以下SQL:
SELECT order_id FROM t_order ORDER BY user_id;
由於原始SQL中並不包含需要在結果歸並中需要獲取的user_id,因此需要對SQL進行補列改寫。補列之后的SQL
是:
SELECT order_id, user_id AS ORDER_BY_DERIVED_0 FROM t_order ORDER BY user_id;
SQL執行
Sharding-JDBC采用一套自動化的執行引擎,負責將路由和改寫完成之后的真實SQL安全且高效發送到底層數據源
執行。 它不是簡單地將SQL通過JDBC直接發送至數據源執行;也並非直接將執行請求放入線程池去並發執行。它
更關注平衡數據源連接創建以及內存占用所產生的消耗,以及最大限度地合理利用並發等問題。 執行引擎的目標是
自動化的平衡資源控制與執行效率,他能在以下兩種模式自適應切換:
內存限制模式
使用此模式的前提是, Sharding-JDBC對一次操作所耗費的數據庫連接數量不做限制。 如果實際執行的SQL需要對
某數據庫實例中的200張表做操作,則對每張表創建一個新的數據庫連接,並通過多線程的方式並發處理,以達成
執行效率最大化。
連接限制模式
使用此模式的前提是,Sharding-JDBC嚴格控制對一次操作所耗費的數據庫連接數量。 如果實際執行的SQL需要對
某數據庫實例中的200張表做操作,那么只會創建唯一的數據庫連接,並對其200張表串行處理。 如果一次操作中
的分片散落在不同的數據庫,仍然采用多線程處理對不同庫的操作,但每個庫的每次操作仍然只創建一個唯一的數
據庫連接。
內存限制模式適用於OLAP操作,可以通過放寬對數據庫連接的限制提升系統吞吐量; 連接限制模式適用於OLTP操
作,OLTP通常帶有分片鍵,會路由到單一的分片,因此嚴格控制數據庫連接,以保證在線系統數據庫資源能夠被
更多的應用所使用,是明智的選擇。
.結果歸並
將從各個數據節點獲取的多數據結果集,組合成為一個結果集並正確的返回至請求客戶端,稱為結果歸並。
Sharding-JDBC支持的結果歸並從功能上可分為遍歷、排序、分組、分頁和聚合5種類型,它們是組合而非互斥的
關系。
歸並引擎的整體結構划分如下圖。
結果歸並從結構划分可分為流式歸並、內存歸並和裝飾者歸並。流式歸並和內存歸並是互斥的,裝飾者歸並可以在
流式歸並和內存歸並之上做進一步的處理。
內存歸並很容易理解,他是將所有分片結果集的數據都遍歷並存儲在內存中,再通過統一的分組、排序以及聚合等
計算之后,再將其封裝成為逐條訪問的數據結果集返回
流式歸並 是指每一次從數據庫結果集中獲取到的數據,都能夠通過游標逐條獲取的方式返回正確的單條數據,它與
數據庫原生的返回結果集的方式最為契合。
下邊舉例說明排序歸並的過程,如下圖是一個通過分數進行排序的示例圖,它采用流式歸並方式。 圖中展示了3張
表返回的數據結果集,每個數據結果集已經根據分數排序完畢,但是3個數據結果集之間是無序的。 將3個數據結
果集的當前游標指向的數據值進行排序,並放入優先級隊列,t_score_0的第一個數據值最大,t_score_2的第一個
數據值次之,t_score_1的第一個數據值最小,因此優先級隊列根據t_score_0,t_score_2和t_score_1的方式排序
隊列。
下圖則展現了進行next調用的時候,排序歸並是如何進行的。 通過圖中我們可以看到,當進行第一次next調用
時,排在隊列首位的t_score_0將會被彈出隊列,並且將當前游標指向的數據值(也就是100)返回至查詢客戶端,
並且將游標下移一位之后,重新放入優先級隊列。 而優先級隊列也會根據t_score_0的當前數據結果集指向游標的
數據值(這里是90)進行排序,根據當前數值,t_score_0排列在隊列的最后一位。 之前隊列中排名第二的
t_score_2的數據結果集則自動排在了隊列首位。
在進行第二次next時,只需要將目前排列在隊列首位的t_score_2彈出隊列,並且將其數據結果集游標指向的值返
回至客戶端,並下移游標,繼續加入隊列排隊,以此類推。 當一個結果集中已經沒有數據了,則無需再次加入隊
列。
可以看到,對於每個數據結果集中的數據有序,而多數據結果集整體無序的情況下,Sharding-JDBC無需將所有的
數據都加載至內存即可排序。 它使用的是流式歸並的方式,每次next僅獲取唯一正確的一條數據,極大的節省了
內存的消耗。
裝飾者歸並是對所有的結果集歸並進行統一的功能增強,比如歸並時需要聚合SUM前,在進行聚合計算前,都會通
過內存歸並或流式歸並查詢出結果集。因此,聚合歸並是在之前介紹的歸並類型之上追加的歸並能力,即裝飾者模
式。
總結
通過以上內容介紹,相信大家已經了解到Sharding-JDBC基礎概念、核心功能以及執行原理。
基礎概念:邏輯表,真實表,數據節點,綁定表,廣播表,分片鍵,分片算法,分片策略,主鍵生成策略
核心功能:數據分片,讀寫分離
執行流程: SQL 解析 => 查詢優化 => SQL路由 => SQL改寫 => SQL執行 => 結果歸並