分庫分表之第三篇

@TOC

3. Sharding-JDBC執(zhí)行原理

3.1 基本概念

在了解Sharding-JDBC的執(zhí)行原理前,需要了解以下概念 :
邏輯表
水平拆分的數(shù)據(jù)表的總稱。例 :訂單數(shù)據(jù)表根據(jù)主鍵尾數(shù)拆分為1-張表,分別是t_order_0、t_order_1到t_order_9,他們的邏輯表名為t_order。
真實表
在分片的數(shù)據(jù)庫中真實存在的物理表。即上個實例中的t_order_0到t_order_9。
數(shù)據(jù)節(jié)點
數(shù)據(jù)分片的最小物理單元。由數(shù)據(jù)源名稱和數(shù)據(jù)表組成,例如 :ds_0.t_order_0。
綁定表
指分片規(guī)則一致的主表和子表。例如 :t_order表和t_order_item表,均按照order_id分片,綁定表之間的分區(qū)鍵完全相同,則此兩張表互為綁定表關(guān)系。綁定表之間的多表關(guān)聯(lián)查詢不會出現(xiàn)笛卡爾積關(guān)聯(lián),關(guān)聯(lián)查詢效率將大大提升。舉例說明,如果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);

在不配置綁定表關(guān)系時,假設(shè)分片鍵order_id將數(shù)值10路由至第0片,將數(shù)值11路由至第1片,那么路由后的SQL應(yīng)該為4條,它們呈現(xiàn)為笛卡爾積 :

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);

在配置綁定表關(guān)系后,路由的SQL應(yīng)該為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);

廣播表
指所有的分片數(shù)據(jù)源中都存在的表,表結(jié)構(gòu)和表中的數(shù)據(jù)在每個數(shù)據(jù)庫中均完全一致。適用于數(shù)據(jù)量不大且需要與海量數(shù)據(jù)的表進行關(guān)聯(lián)查詢的場景,例如 :字典表。
分片鍵
用于分片的數(shù)據(jù)庫字段,是將數(shù)據(jù)庫(表)水平拆分的關(guān)鍵字段。例如 :將訂單表中的訂單主鍵的尾數(shù)取模分片,則訂單主鍵為分片字段。SQL中如果無分片字段,將執(zhí)行全路由,性能較差。除了對單分片字段的支持,Sharding-JDBC也支持根據(jù)多個字段進行分片。
分片算法
通過分片算法將數(shù)據(jù)分片,支持通過=、BETWZEEN和IN分片。分片算法需要應(yīng)用方開發(fā)者自行實現(xiàn),可實現(xiàn)的靈活度非常高。包括 :精確分片算法、范圍分片算法、復(fù)合分片算法等。例如 :where order_id = ?將采用精確分片算法,where order_id in (?,?,?)將采用精確分片算法,where order_id BETWEEN ?and ?將采用范圍分片算法,復(fù)合分片算法用于分片鍵有多個復(fù)雜情況。
分片策略
包含分片鍵和分片算法,由于分片算法的獨立性,將其獨立抽離。真正可用于分片操作的是分片鍵 + 分片算法,也就是分片策略。內(nèi)置的分片策略大致可分為尾數(shù)取模、哈希、范圍、標(biāo)簽、時間等。由用戶方配置的分片策略則更加靈活,常用的使用行表達(dá)式配置分片策略,它采用Groovy表達(dá)式表示 :如 :t_user_$->{u_id % 8}表示t_user表根據(jù)u_id摸8,而分成8張表,表名稱為t_user_0到t_user_7。
自增主鍵生成策略
通過在客戶端生成自增主鍵替換以數(shù)據(jù)庫原生自增主鍵的方式,做到分布式主鍵無重復(fù)。

3.2. SQL解析

當(dāng)Sharding-JDBC接受到一條SQL語句時,會陸續(xù)執(zhí)行SQL解析 =》查詢優(yōu)化 =》SQL路由 =》SQL改寫 =》結(jié)果歸并,最終返回執(zhí)行結(jié)果。

在這里插入圖片描述

SQL解析過程分為詞法解析語法解析。詞法解析器用于將SQL拆解為不可再分的院子符號,稱為Token。并根據(jù)不同數(shù)據(jù)庫方言所提供的字典,將其歸類為關(guān)鍵字、表達(dá)式、字面量和操作符。再使用語法解析器將SQL轉(zhuǎn)換為抽象語法樹。
例如,以下SQL:

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

解析之后的為抽象語法樹見下圖 :

在這里插入圖片描述

為了便于理解,抽象語法樹中的關(guān)鍵字的Token用綠色表示,變量的Token用紅色表示,灰色表示需要進一步拆分。
最后,通過對抽象語法樹的遍歷去提煉分片所需的上下文,并標(biāo)記有可能需要SQL改寫(后邊介紹)的位置。供分片使用的解析上下文包含查詢選擇項(Select Items)、表信息(Table)、分片條件(Sharding Condition)、自增主鍵信息(Auto increment Primary Key)、排序信息(Order By)、分組信息(Group By)以及分頁信息(Limit、Rownum、Top)。

3.3.SQL路由

SQL路由就是把針對邏輯表的數(shù)據(jù)操作映射到對數(shù)據(jù)結(jié)點操作的過程。
根據(jù)解析上下文匹配數(shù)據(jù)庫和表的分片策略,并生成路由路徑。對于攜帶分片鍵的SQL,根據(jù)分片鍵操作符不同可以劃分為單片路由(分片鍵的操作符是等號)、多片路由(分片鍵的操作符是IN)和范圍路由(分片鍵的操作符是BETWEEN),不攜帶分片鍵的SQL則采用廣播路由。根據(jù)分片鍵進行路由的場景可分為直接路由、標(biāo)準(zhǔn)路由、笛卡爾積路由等。
標(biāo)準(zhǔn)路由
標(biāo)準(zhǔn)路由是Sharding-JDBC最為推薦使用的分片方式,它的使用范圍是不包含關(guān)聯(lián)查詢或僅包含綁定表之間關(guān)聯(lián)查詢的SQL。當(dāng)分片運算符是等于號時,路由結(jié)果將落入單庫(表),當(dāng)分片運算符是BETWEEN或IN時,則路由結(jié)果不一定落入唯一的庫(表),因此這條邏輯SQL最終可能被拆分為多條用于執(zhí)行的真實SQL。舉例說明,如果按照order_id的奇數(shù)和偶數(shù)進行數(shù)據(jù)分片,一個單表查詢的SQL如下 :

SELECT * FROM t_order WHERE order_id IN (1, 2);

那么路由的結(jié)果應(yīng)為 :

SELECT * FROM t_order_0 WHERE order_id IN (1, 2);
SELECT * FROM t_order_1 WHERE order_id IN (1, 2);

綁定表的關(guān)聯(lián)查詢與單表查詢復(fù)雜度和性能相當(dāng)。舉例說明,如果一個包含綁定表的關(guān)聯(lián)查詢的SQL如下 :

SELECT * FROM t_order o JOIN t_order_item i ON o.order_id=i.order_id WHERE order_id IN (1, 2);

那么路由的結(jié)果應(yīng)為 :

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拆分的數(shù)目與單表是一致的。
笛卡爾路由
笛卡爾路由是最復(fù)雜的情況,它無法根據(jù)綁定表的關(guān)系定位分片規(guī)則,因此非綁定表之間的關(guān)聯(lián)查詢需要拆解為笛卡爾積組合執(zhí)行。如果上個示例中的SQL并未配置綁定表關(guān)系,那么路由的結(jié)果應(yīng)為 :

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);

笛卡爾路由查詢性能較低,需謹(jǐn)慎使用。
全庫表路由
對于不攜帶分片鍵的SQL,則采用廣播路由的方式。根據(jù)SQL類型又可以劃分為全庫表路由、全庫路由、全實例路由、單播路由和阻斷路由這5種類型。其中全庫表路由用于處理對數(shù)據(jù)庫中與其邏輯表相關(guān)的所有真實表的操作,主要包括不帶分片鍵的DQL(數(shù)據(jù)查詢)和DML(數(shù)據(jù)操縱),以及DDL(數(shù)據(jù)定義)等。例如 :

SELECT * FROM t_order WHERE good_prority IN (1, 10);

則會遍歷所有數(shù)據(jù)庫中的所有表,逐一匹配邏輯表和真實表名,能夠匹配得上則執(zhí)行。路由后成為

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);

3.4. SQL改寫

工程師面向邏輯表書寫的SQL,并不能夠直接在真實的數(shù)據(jù)庫中執(zhí)行,SQL改寫用于將邏輯SQL改寫為在真實數(shù)據(jù)庫中可以正確執(zhí)行的SQL。
如一個簡單的例子,若邏輯SQL為 :

SELECT order_id FROM t_order WHERE order_id=1;

假設(shè)該SQL配置分片鍵order_id,并且order_id=1的情況,將路由至分片表1。那么改寫之后的SQL應(yīng)該為 :

SELECT order_id FROM t_order_1 WHERE order_id=1;

再比如,Sharding-JDBC需要在結(jié)果歸并時獲取相應(yīng)數(shù)據(jù),但該數(shù)據(jù)并未能通過查詢的SQL返回。這種情況主要是針對GROUP BY和ORDER BY。結(jié)果歸并時,需要根據(jù)GROUP_BY和ORDER_BY的字段項進行分組和排序,但如果原始SQL的選擇項中若并未包含分組項或排序項,則需要對原始SQL進行改寫。先看一下原始SQL中帶有結(jié)果歸并所需信息的場景 :

SELECT order_id, user_id FROM t_order ORDER BY user_id;

由于user_id進行排序,在結(jié)果歸并中需要能夠獲取到user_id的數(shù)據(jù),而上面的SQL是能夠獲取到user_id獲取的,因此無需補列。
如果選擇項中不包含結(jié)果歸并時所需的列,則需要進行補列,如以下SQL :

SELECT order_id FROM t_order ORDER BY user_id;

由于原始SQL中并不包含需要在結(jié)果歸并中需要獲取的user_id,因此需要對SQL進行補列改寫。補列之后的SQL

SELECT order_id, user_id AS ORDER_BY_DERIVED_0 FROM t_order ORDER BY user_id;

3.6.結(jié)果歸并

將從各個數(shù)據(jù)節(jié)點獲取的多數(shù)據(jù)結(jié)果集,組合成為一個結(jié)果集并正確的返回至請求客戶端,稱為結(jié)果歸并。
Sharding-JDBC支持的結(jié)果歸并從功能上可分為遍歷、排序、分組、分頁聚合5種類型,它們是組合而非互斥的關(guān)系。
歸并引擎的整體結(jié)構(gòu)劃分如下圖 。

在這里插入圖片描述

結(jié)果歸并從結(jié)構(gòu)劃分可分為流式歸并內(nèi)存歸并裝飾者歸并。流式歸并和內(nèi)存歸并是互斥的,裝飾者歸并可以在流式歸并和內(nèi)存歸并之上做進一步的處理。
內(nèi)存歸并很容易理解,他是將所有分片結(jié)果集的數(shù)據(jù)都遍歷并存儲在內(nèi)存中,再通過統(tǒng)一的分組、排序以及聚合等計算之后,再將其封裝成為逐條訪問的數(shù)據(jù)結(jié)果集返回。

流式歸并是指每一次從數(shù)據(jù)庫結(jié)果集中獲取到的數(shù)據(jù),都能夠通過游標(biāo)逐條獲取的方式返回正確的單條數(shù)據(jù),它與數(shù)據(jù)庫原生的返回結(jié)果集的方式最為契合。
下邊舉例說明排序歸并的過程,如下圖是一個通過分?jǐn)?shù)進行排序的示例圖,它采用流式歸并方式。圖中展示列3張表返回的數(shù)據(jù)結(jié)果集,每個數(shù)據(jù)結(jié)果集已經(jīng)根據(jù)分?jǐn)?shù)排序完畢,但是3個數(shù)據(jù)結(jié)果集之間是無序的。將3個數(shù)據(jù)結(jié)果集的當(dāng)前游標(biāo)指向的數(shù)據(jù)值進行排序,并放入優(yōu)先級隊列,t_score_0的第一個數(shù)據(jù)值最大,t_score_2的第一個數(shù)據(jù)值次之,t_score_1的第一個數(shù)據(jù)值最小,因此優(yōu)先級隊列根據(jù)t_score_0、t_score_2和t_score_1的方式排序隊列。

在這里插入圖片描述

下圖則展現(xiàn)了進行next調(diào)用的時候,排序歸并是如何進行的。通過圖中我們可以看到,當(dāng)進行第一次next調(diào)用時,排在隊列首位的t_score_0將會被彈出隊列,并且將當(dāng)前游標(biāo)指向的數(shù)據(jù)值(也就是100)返回至查詢客戶端,并且將游標(biāo)下移一位之后,重新放入優(yōu)先級隊列。而優(yōu)先級隊列也會根據(jù)t_score_0的當(dāng)前數(shù)據(jù)結(jié)果集指向游標(biāo)的數(shù)據(jù)值(這里是90)進行排序,根據(jù)當(dāng)前數(shù)值,t_score_0排列在隊列的最后一位。之前隊列中排名第二的t_score_2的數(shù)據(jù)結(jié)果集則自動排在隊列首位。
在進行第二次next時,只需要將目標(biāo)排列在隊列首位的t_score_2彈出隊列,并且將其數(shù)據(jù)結(jié)果集游標(biāo)指向的值返回至客戶端,并下移游標(biāo),繼續(xù)加入隊列排隊,以此類推。當(dāng)一個結(jié)果集中已經(jīng)沒有數(shù)據(jù)了,則無需再次加入隊列。
在這里插入圖片描述

可以看到,對于每個數(shù)據(jù)結(jié)果集中的數(shù)據(jù)有序,而多數(shù)據(jù)結(jié)果集整體無序的情況下,Sharding-JDBC無需將所有的數(shù)據(jù)都加載至內(nèi)存即可排序。它使用的是流式歸并的方式,每次next僅獲取唯一正確的一條數(shù)據(jù),極大的節(jié)省了內(nèi)存的消耗。

裝飾者歸并是對所有的結(jié)果集歸并進行統(tǒng)一的功能增強,比如歸并時需要聚合SUM前,在進行聚合計算前,都會通過內(nèi)存歸并或流式歸并查詢出結(jié)果集。因此,聚合歸并是在之前介紹的歸并類型之上追加的歸并能力,即裝飾者模式。

3.7 總結(jié)

通過以上內(nèi)容介紹,相信大家已經(jīng)了解到Sharding-JDBC基礎(chǔ)概念、核心功能以及執(zhí)行原理。
基礎(chǔ)概念 :邏輯表、真實表、數(shù)據(jù)節(jié)點、綁定表、廣播表、分片鍵、分片算法、分片策略、主鍵生成策略
核心功能 :數(shù)據(jù)分片、讀寫分離
執(zhí)行流程 :SQL解析 =》查詢優(yōu)化 =》SQL路由 =》SQL改寫 =》SQL執(zhí)行 =》結(jié)果歸并

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容