天天看點

sharding-jdbc執行原理

當Sharding-JDBC接受到一條SQL語句時,會陸續執行 SQL解析 -> 查詢優化 -> SQL路由 -> SQL改寫 -> SQL執行 ->結果歸并 ,最終傳回執行結果。 

sharding-jdbc執行原理

SQL解析

SQL解析過程分為詞法解析和文法解析。 詞法解析器用于将SQL拆解為不可再分的原子符号,稱為Token。并根據不同資料庫方言所提供的字典,将其歸類為關鍵字,表達式,字面量和操作符。 再使用文法解析器将SQL轉換為抽象文法樹。

例如,以下SQL:

SELECT id, name FROM t_user WHERE status = 'ACTIVE' AND age > 18       

解析之後的為抽象文法樹見下圖:

sharding-jdbc執行原理

為了便于了解,抽象文法樹中的關鍵字的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種類型,它們是組合而非互斥的關系。

歸并引擎的整體結構劃分如下圖。

sharding-jdbc執行原理

結果歸并從結構劃分可分為流式歸并、記憶體歸并和裝飾者歸并。流式歸并和記憶體歸并是互斥的,裝飾者歸并可以在流式歸并和記憶體歸并之上做進一步的處理。

記憶體歸并

很容易了解,他是将所有分片結果集的資料都周遊并存儲在記憶體中,再通過統一的分組、排序以及聚合等計算之後,再将其封裝成為逐條通路的資料結果 

流式歸并

是指每一次從資料庫結果集中擷取到的資料,都能夠通過遊标逐條擷取的方式傳回正确的單條資料,它與資料庫原生的傳回結果集的方式最為契合。

下邊舉例說明排序歸并的過程,如下圖是一個通過分數進行排序的示例圖,它采用流式歸并方式。 圖中展示了3張表傳回的資料結果集,每個資料結果集已經根據分數排序完畢,但是3個資料結果集之間是無序的。 将3個資料結果集的目前遊标指向的資料值進行排序,并放入優先級隊列,t_score_0的第一個資料值最大,t_score_2的第一個資料值次之,t_score_1的第一個資料值最小,是以優先級隊列根據t_score_0,t_score_2和t_score_1的方式排序隊列。 

sharding-jdbc執行原理

下圖則展現了進行next調用的時候,排序歸并是如何進行的。 通過圖中我們可以看到,當進行第一次next調用時,排在隊列首位的t_score_0将會被彈出隊列,并且将目前遊标指向的資料值(也就是100)傳回至查詢用戶端,并且将遊标下移一位之後,重新放入優先級隊列。 而優先級隊列也會根據t_score_0的目前資料結果集指向遊标的資料值(這裡是90)進行排序,根據目前數值,t_score_0排列在隊列的最後一位。 之前隊列中排名第二的t_score_2的資料結果集則自動排在了隊列首位。

在進行第二次next時,隻需要将目前排列在隊列首位的t_score_2彈出隊列,并且将其資料結果集遊标指向的值傳回至用戶端,并下移遊标,繼續加入隊列排隊,以此類推。 當一個結果集中已經沒有資料了,則無需再次加入隊列。 

sharding-jdbc執行原理

可以看到,對于每個資料結果集中的資料有序,而多資料結果集整體無序的情況下,Sharding-JDBC無需将所有的資料都加載至記憶體即可排序。 它使用的是流式歸并的方式,每次next僅擷取唯一正确的一條資料,極大的節省了記憶體的消耗。

裝飾者歸并

是對所有的結果集歸并進行統一的功能增強,比如歸并時需要聚合SUM前,在進行聚合計算前,都會通過記憶體歸并或流式歸并查詢出結果集。是以,聚合歸并是在之前介紹的歸并類型之上追加的歸并能力,即裝飾者模式。 

繼續閱讀