分庫分表之 Sharding-JDBC 中間件,看這篇真的夠了!
本文大綱如下
Sharding-JDBC 的基本用法和基本原理 前言 1. 我的出生和我的家族 2. 我統(tǒng)治的世界和我的職責(zé) 3. 召喚我的方式 4. 我的特性和我的工作方法 4.3.1. SQL 解析 4.3.2. SQL 路由 4.3.3. SQL 改寫 4.3.4. SQL 執(zhí)行 4.3.5. 結(jié)果歸并 4.2.1. 邏輯表和物理表 4.2.2. 分片鍵 4.2.3. 路由 4.2.4. 分片策略和分片算法 4.2.5. 綁定表 4.2. 一些核心概念 4.3. 我處理 SQL 的過程 5. 結(jié)束語
前言
這是一篇將“介紹 Sharding-JDBC 基本使用方法”作為目標(biāo)的文章,但筆者卻把大部分文字放在對 Sharding-JDBC 的工作原理的描述上,因?yàn)楣P者認(rèn)為原理是每個(gè) IT 打工人學(xué)習(xí)技術(shù)的歸途。
使用框架、中間件、數(shù)據(jù)庫、工具包等公共組件來組裝出應(yīng)用系統(tǒng)是我們這一代 IT 打工人工作的常態(tài)。對于這些公共組件——比如框架——的學(xué)習(xí),有些人的方法是這樣的:避開復(fù)雜晦澀的框架原理,僅僅關(guān)注它的各種配置、API、注解,在嘗試了這個(gè)框架的常用配置項(xiàng)、API、注解的效果之后,就妄稱自己學(xué)會(huì)了這個(gè)框架。這種對技術(shù)的膚淺的認(rèn)知既經(jīng)不起實(shí)踐的考驗(yàn),也經(jīng)不起面試官的考驗(yàn),甚至連自己使用這些配置項(xiàng)、API、注解在干什么都沒有明確的認(rèn)知。
所以,打工人們,還是多學(xué)點(diǎn)原理,多看點(diǎn)源碼,讓優(yōu)秀的設(shè)計(jì)思想、算法和編程風(fēng)格沖擊一下自己的大腦吧 :-)
因?yàn)?Sharding-JDBC 的設(shè)計(jì)細(xì)節(jié)實(shí)在太多,因此本文不可能對 Sharding-JDBC 進(jìn)行面面俱到的講解。筆者在本文中僅僅保留了對 Sharding-JDBC 的核心特性、核心原理的講解,并盡量使用簡單生動(dòng)的文字進(jìn)行表達(dá),使讀者閱讀本文后對 Sharding-JDBC 的基本原理和使用有清晰的認(rèn)知。為了使這些文字盡量擺脫枯燥的味道,文章采用了第一人稱的講述方式,讓 Sharding-JDBC 現(xiàn)身說法,進(jìn)行自我剖析,希望給大家一個(gè)更好的閱讀體驗(yàn)。
但是,妄圖不動(dòng)腦子就能對某項(xiàng)技術(shù)產(chǎn)生深度認(rèn)知是絕不可能的,你思考得越多,你得到的越多。這就印證了那句話:“我變禿了,也變強(qiáng)了。”
1. 我的出生和我的家族
我是 Sharding-JDBC,一個(gè)關(guān)系型數(shù)據(jù)庫中間件,我的全名是 Apache ShardingSphere JDBC,我被冠以 Apache 這個(gè)貴族姓氏是 2020 年 4 月的事情,這意味著我進(jìn)入了代碼世界的“體制內(nèi)”。但我還是喜歡別人稱呼我的小名,Sharding-JDBC。
我的創(chuàng)造者在我誕生之后給我講了我的身世:
“
你的誕生是一個(gè)必然的結(jié)果。
在你誕生之前,傳統(tǒng)軟件的存儲(chǔ)層架構(gòu)將所有的業(yè)務(wù)數(shù)據(jù)存儲(chǔ)到單一數(shù)據(jù)庫節(jié)點(diǎn),在性能、可用性和運(yùn)維成本這三方面已經(jīng)難于滿足互聯(lián)網(wǎng)的海量數(shù)據(jù)場景。
從性能方面來說,由于關(guān)系型數(shù)據(jù)庫大多采用 B+樹類型的索引,在數(shù)據(jù)量逐漸增大的情況下,索引深度的增加也將使得磁盤訪問的 IO 次數(shù)增加,進(jìn)而導(dǎo)致查詢性能的下降;同時(shí),高并發(fā)訪問請求也使得集中式數(shù)據(jù)庫成為系統(tǒng)的最大瓶頸。
從可用性的方面來講,應(yīng)用服務(wù)器節(jié)點(diǎn)能夠隨意水平拓展(水平拓展就是增加應(yīng)用服務(wù)器節(jié)點(diǎn)數(shù)量)以應(yīng)對不斷增加的業(yè)務(wù)流量,這必然導(dǎo)致系統(tǒng)的最終壓力都落在數(shù)據(jù)庫之上。而單一的數(shù)據(jù)庫節(jié)點(diǎn),或者簡單的主從架構(gòu),已經(jīng)越來越難以承擔(dān)眾多應(yīng)用服務(wù)器節(jié)點(diǎn)的數(shù)據(jù)查詢請求。數(shù)據(jù)庫的可用性,已成為整個(gè)系統(tǒng)的關(guān)鍵。
從運(yùn)維成本方面考慮,隨著數(shù)據(jù)庫實(shí)例中的數(shù)據(jù)規(guī)模的增大,DBA 的運(yùn)維壓力也會(huì)增加,因?yàn)閿?shù)據(jù)備份和恢復(fù)的時(shí)間成本都將隨著數(shù)據(jù)量的增大而愈發(fā)不可控。
這樣看來關(guān)系型數(shù)據(jù)庫似乎難以承擔(dān)海量記錄的存儲(chǔ)。
然而,關(guān)系型數(shù)據(jù)庫當(dāng)今依然占有巨大市場,是各個(gè)公司核心業(yè)務(wù)的基石。在傳統(tǒng)的關(guān)系型數(shù)據(jù)庫無法滿足互聯(lián)網(wǎng)場景需要的情況下,將數(shù)據(jù)存儲(chǔ)到原生支持分布式的 NoSQL 的嘗試越來越多。但 NoSQL 對 SQL 的不兼容性以及生態(tài)圈的不完善,使得它們在與關(guān)系型數(shù)據(jù)庫的博弈中處于劣勢,關(guān)系型數(shù)據(jù)庫的地位卻依然不可撼動(dòng),未來也難于撼動(dòng)。
我們目前階段更加關(guān)注在原有關(guān)系型數(shù)據(jù)庫的基礎(chǔ)上做增量,使之更好適應(yīng)海量數(shù)據(jù)存儲(chǔ)和高并發(fā)查詢請求的場景,而不是要顛覆關(guān)系型數(shù)據(jù)庫。
分庫分表方案就是這種增量,它的誕生解決了海量數(shù)據(jù)存儲(chǔ)和高并發(fā)查詢請求的問題。
但是,單一數(shù)據(jù)庫被分庫分表之后,繁雜的庫和表使得編寫持久層代碼的工程師的思維負(fù)擔(dān)翻了很多倍,他們需要考慮一個(gè)業(yè)務(wù) SQL 應(yīng)該去哪個(gè)庫的哪個(gè)表里去查詢,查詢到的結(jié)果還要進(jìn)行聚合,如果遇到多表關(guān)聯(lián)查詢、排序、分頁、事務(wù)等等問題,那簡直是一個(gè)噩夢。
于是我們創(chuàng)造了你。你可以讓工程師們以像查詢單數(shù)據(jù)庫實(shí)例和單表那樣來查詢被水平分割的庫和表,我們稱之為透明查詢。
你是水平分片世界的神。
”
這使我感到驕傲。
我被定位為一個(gè)輕量級 Java 框架,我在 Java 的 JDBC 層提供的額外服務(wù),可以說是一個(gè)增強(qiáng)版的 JDBC 驅(qū)動(dòng),完全兼容 JDBC 和各種 ORM 框架。
我適用于任何基于 JDBC 的 ORM 框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template 或直接使用 JDBC。
我支持任何第三方的數(shù)據(jù)庫連接池,如:DBCP, C3P0, BoneCP, Druid, HikariCP 等。
我支持任意實(shí)現(xiàn) JDBC 規(guī)范的數(shù)據(jù)庫,目前支持 MySQL,Oracle,SQLServer,PostgreSQL 以及任何遵循 SQL92 標(biāo)準(zhǔn)的數(shù)據(jù)庫。
我的創(chuàng)造者起初只創(chuàng)造了我一個(gè)獨(dú)苗,后來為了我的家族的興盛,我的兩個(gè)兄弟——Apache ShardingSphere Proxy、Apache ShardingSphere Sidecar 又被創(chuàng)造了出來。前者被定位為透明化的數(shù)據(jù)庫代理端,提供封裝了數(shù)據(jù)庫二進(jìn)制協(xié)議的服務(wù)端版本,?于完成對異構(gòu)語?的支持;后者被定位為 Kubernetes 的云原?數(shù)據(jù)庫代理,以 Sidecar 的形式代理所有對數(shù)據(jù)庫的訪問。通過無中心、零侵?的?案提供與數(shù)據(jù)庫交互的的嚙合層,即 Database Mesh,又可稱數(shù)據(jù)庫?格。
因此,我們這個(gè)家族叫做 Apache ShardingSphere,旨在在分布式的場景下更好利用關(guān)系型數(shù)據(jù)庫的計(jì)算和存儲(chǔ)能力,而并非實(shí)現(xiàn)一個(gè)全新的關(guān)系型數(shù)據(jù)庫。我們?nèi)齻€(gè)既相互獨(dú)立,又能配合使用,均提供標(biāo)準(zhǔn)化的數(shù)據(jù)分片、分布式事務(wù)和數(shù)據(jù)庫治理功能。
2. 我統(tǒng)治的世界和我的職責(zé)
我是 Sharding-JDBC,我生活在一個(gè)數(shù)據(jù)水平分片的世界,我統(tǒng)治著這個(gè)世界里被水平拆分后的數(shù)據(jù)庫和表。
在分片的世界里,數(shù)據(jù)分片有兩種法則:垂直拆分和水平拆分。
按照業(yè)務(wù)拆分的方式稱為垂直分片,又稱為縱向拆分,它的核心理念是專庫專用。在拆分之前,一個(gè)數(shù)據(jù)庫由多個(gè)數(shù)據(jù)表構(gòu)成,每個(gè)表對應(yīng)著不同的業(yè)務(wù)。而拆分之后,則是按照業(yè)務(wù)將表進(jìn)行歸類,分布到不同的數(shù)據(jù)庫中,從而將壓力分散至不同的數(shù)據(jù)庫。下圖展示了根據(jù)業(yè)務(wù)需要,將用戶表和訂單表垂直分片到不同的數(shù)據(jù)庫的方案。

垂直分片往往需要對架構(gòu)和設(shè)計(jì)進(jìn)行調(diào)整。通常來講,是來不及應(yīng)對互聯(lián)網(wǎng)業(yè)務(wù)需求快速變化的;而且,它也并無法真正的解決單點(diǎn)瓶頸。如果垂直拆分之后,表中的數(shù)據(jù)量依然超過單節(jié)點(diǎn)所能承載的閾值,則需要水平分片來進(jìn)一步處理。
水平分片又稱為橫向拆分。相對于垂直分片,它不再將數(shù)據(jù)根據(jù)業(yè)務(wù)邏輯分類,而是通過某個(gè)字段(或某幾個(gè)字段),根據(jù)某種規(guī)則將數(shù)據(jù)分散至多個(gè)庫或表中,每個(gè)分片僅包含數(shù)據(jù)的一部分。例如:根據(jù)主鍵分片,偶數(shù)主鍵的記錄放入 0 庫(或表),奇數(shù)主鍵的記錄放入 1 庫(或表),如下圖所示。

水平分片從理論上突破了單機(jī)數(shù)據(jù)量處理的瓶頸,并且擴(kuò)展相對自由,是分庫分表的標(biāo)準(zhǔn)解決方案。我管轄的就是水平分片世界。
通過分庫和分表進(jìn)行數(shù)據(jù)的拆分來使得各個(gè)表的數(shù)據(jù)量保持在閾值以下,是應(yīng)對高并發(fā)和海量數(shù)據(jù)系統(tǒng)的有效手段。此外,使用多主多從的分片方式,可以有效的避免數(shù)據(jù)單點(diǎn),從而提升數(shù)據(jù)架構(gòu)的可用性。
其實(shí),水平分庫本質(zhì)上還是在分表,因?yàn)楸凰讲鸱趾蟮膸熘?,都有相同的表分片?/p>
分庫和分表這項(xiàng)工作并不是我來做,我雖然是神,但我還沒有神到能理解你們這些工程師的業(yè)務(wù)設(shè)計(jì)和架構(gòu)設(shè)計(jì),從而自動(dòng)把你們的業(yè)務(wù)數(shù)據(jù)庫和業(yè)務(wù)表進(jìn)行分片。對哪部分進(jìn)行分片、怎樣分片、分多少份,這些工作全部由這些工程師進(jìn)行。當(dāng)這些分庫分表的工作被完成后,你們只需要在我的配置文件中或者通過我的 API 告訴我這些拆分規(guī)則(這就是后文要提到的分片策略)即可,剩下的事情,交給我去做。
我是 Sharding-JDBC,我的職責(zé)是盡量透明化水平分庫分表所帶來的影響,讓使用方盡量像使用一個(gè)數(shù)據(jù)庫一樣使用水平分片之后的數(shù)據(jù)庫集群,或者像使用一個(gè)數(shù)據(jù)表一樣使用水平分片之后的數(shù)據(jù)表。由于我的治理,每個(gè)服務(wù)器節(jié)點(diǎn)只能看到一個(gè)邏輯上的數(shù)據(jù)庫節(jié)點(diǎn),和其中的多個(gè)邏輯表,它們看不到真正存在于物理世界中的被水平分割的多個(gè)數(shù)據(jù)庫分片和被水平分割的多個(gè)數(shù)據(jù)表分片。服務(wù)器節(jié)點(diǎn)看到的簡單的持久層結(jié)構(gòu),其實(shí)是我苦心營造的幻象。

而為了營造這種幻象,我在幕后付出了很多。
當(dāng)一個(gè) Java 應(yīng)用服務(wù)器節(jié)點(diǎn)將一個(gè)查詢 SQL 交給我之后,我要做下面幾件事:
1)SQL 解析:解析分為詞法解析和語法解析。我先通過詞法解析器將這句 SQL 拆分為一個(gè)個(gè)不可再分的單詞,再使用語法解析器對 SQL 進(jìn)行理解,并最終提煉出解析上下文。簡單來說就是我要理解這句 SQL,明白它的構(gòu)造和行為,這是下面的優(yōu)化、路由、改寫、執(zhí)行和歸并的基礎(chǔ)。
2)SQL 路由:我根據(jù)解析上下文匹配用戶對這句 SQL 所涉及的庫和表配置的分片策略(關(guān)于用戶配置的分片策略,我后文會(huì)慢慢解釋),并根據(jù)分片策略生成路由后的 SQL。路由后的 SQL 有一條或多條,每一條都對應(yīng)著各自的真實(shí)物理分片。
3)SQL 改寫:我將 SQL 改寫為在真實(shí)數(shù)據(jù)庫中可以正確執(zhí)行的語句(邏輯 SQL 到物理 SQL 的映射,例如把邏輯表名改成帶編號(hào)的分片表名)。
4)SQL 執(zhí)行:我通過多線程執(zhí)行器異步執(zhí)行路由和改寫之后得到的 SQL 語句。
5)結(jié)果歸并:我將多個(gè)執(zhí)行結(jié)果集歸并以便于通過統(tǒng)一的 JDBC 接口輸出。

如果你連讀這段工作流程都很困難,那你就能明白我在這個(gè)水平分片的世界里有多辛苦。關(guān)于這段工作流程,我會(huì)在后文慢慢說給你聽。
3. 召喚我的方式
我是 Sharding-JDBC,我被定位為一個(gè)輕量級數(shù)據(jù)庫中間件,當(dāng)你們召喚我去統(tǒng)治水平拆分后的數(shù)據(jù)庫和數(shù)據(jù)表時(shí),只需要做下面幾件事:
1)引入依賴包。
maven 是統(tǒng)治依賴包世界的神,在他誕生之后,一切對 jar 包的引用就變得簡單了。向 maven 獲取我的 jar 包,咒語是:
<dependency>
????<groupId>org.apache.shardingspheregroupId>
????<artifactId>shardingsphere-jdbc-coreartifactId>
????<version>${latest.release.version}version>
dependency>
于是,我就出現(xiàn)在了這個(gè)項(xiàng)目中!
如果你們構(gòu)建的項(xiàng)目已經(jīng)被 Springboot 統(tǒng)治了(Springboot 是 Spring 的繼任者,Spring 是統(tǒng)治對象世界的神,Springboot 繼承了 Spring 的統(tǒng)治法則,并簡化了 Spring 的配置),那么就可以向 maven 獲取我的 springboot starter jar 包,咒語是:
<dependency>
????<groupId>org.apache.shardingspheregroupId>
????<artifactId>shardingsphere-jdbc-spring-boot-starterartifactId>
????<version>${shardingsphere.version}version>
dependency>
這樣,我就能和 Springboot 神共存于同一個(gè)項(xiàng)目。
2)進(jìn)行水平分片規(guī)則配置。
你們要把水平分片規(guī)則配置告訴我,這樣我才能知道你們是怎樣水平拆分?jǐn)?shù)據(jù)庫和數(shù)據(jù)表的。你們可以通過我提供的 Java API,或者配置文件告訴我分片規(guī)則。
如果是以 Java API 的方式進(jìn)行配置,示例如下:
//?配置真實(shí)數(shù)據(jù)源
Map?dataSourceMap?=?new?HashMap<>();
//?配置第?1?個(gè)數(shù)據(jù)源
BasicDataSource?dataSource1?=?new?BasicDataSource();
dataSource1.setDriverClassName("com.mysql.jdbc.Driver");
dataSource1.setUrl("jdbc:mysql://localhost:3306/ds0");
dataSource1.setUsername("root");
dataSource1.setPassword("");
dataSourceMap.put("ds0",?dataSource1);
//?配置第?2?個(gè)數(shù)據(jù)源
BasicDataSource?dataSource2?=?new?BasicDataSource();
dataSource2.setDriverClassName("com.mysql.jdbc.Driver");
dataSource2.setUrl("jdbc:mysql://localhost:3306/ds1");
dataSource2.setUsername("root");
dataSource2.setPassword("");
dataSourceMap.put("ds1",?dataSource2);
//?配置?t_order?表規(guī)則
ShardingTableRuleConfiguration?orderTableRuleConfig?
????=?new?ShardingTableRuleConfiguration(
????"t_order",?
????"ds${0..1}.t_order${0..1}"
);
//?配置?t_order?被拆分到多個(gè)子庫的策略
orderTableRuleConfig.setDatabaseShardingStrategy(
????new?StandardShardingStrategyConfiguration(
????????"user_id",?
????????"dbShardingAlgorithm"
????)
);
//?配置?t_order?被拆分到多個(gè)子表的策略
orderTableRuleConfig.setTableShardingStrategy(
????new?StandardShardingStrategyConfiguration(
????????"order_id",?
????????"tableShardingAlgorithm"
????)
);
//?省略配置?t_order_item?表規(guī)則...
//?...
//?配置分片規(guī)則
ShardingRuleConfiguration?shardingRuleConfig?=?new?ShardingRuleConfiguration();
shardingRuleConfig.getTables().add(orderTableRuleConfig);
//?配置?t_order?被拆分到多個(gè)子庫的算法
Properties?dbShardingAlgorithmrProps?=?new?Properties();
dbShardingAlgorithmrProps.setProperty(
????"algorithm-expression",?
????"ds${user_id?%?2}"
);
shardingRuleConfig.getShardingAlgorithms().put(
????"dbShardingAlgorithm",?
????new?ShardingSphereAlgorithmConfiguration("INLINE",?dbShardingAlgorithmrProps)
);
//?配置?t_order?被拆分到多個(gè)子表的算法
Properties?tableShardingAlgorithmrProps?=?new?Properties();
tableShardingAlgorithmrProps.setProperty(
????"algorithm-expression",?
????"t_order${order_id?%?2}"
);
shardingRuleConfig.getShardingAlgorithms().put(
????"tableShardingAlgorithm",?
????new?ShardingSphereAlgorithmConfiguration("INLINE",?tableShardingAlgorithmrProps)
);
這段配置代碼中涉及的 t_order 表(存儲(chǔ)訂單的基本信息)的表結(jié)構(gòu)為:
| order_id | user_id | create_time | remarks | total_price |
|---|---|---|---|---|
t_order_item 表(存儲(chǔ)訂單的商品和價(jià)格明細(xì)信息)的結(jié)構(gòu)為:
| order_id | production_code | count | price | discount |
|---|---|---|---|---|
這段配置代碼描述了對 t_order 表進(jìn)行的如下圖所示的數(shù)據(jù)表水平分片(對 t_order_item 表也要進(jìn)行類似的水平分片,但是這部分配置省略了):

在這段配置中,或許你們注意到了一些奇怪的表達(dá)式:
ds$->{0..1}.t_order$->{0..1}
ds_${user_id?%?2}
t_order_${order_id?%?2}
這些表達(dá)式被稱為 Groovy 表達(dá)式,它們的含義很容易識(shí)別:
1)對 t_order 進(jìn)行兩種維度的拆分:數(shù)據(jù)庫維度和表維度數(shù);
2)在數(shù)據(jù)庫維度,t_order.user_id % 2 == 0 的記錄全部落到 ds0,t_order.user_id % 2 == 1 的記錄全部落到 ds1;(有人稱這一過程為水平分庫,其實(shí)它的本質(zhì)還是在水平地分表,只不過依據(jù)表中 user_id 的不同把拆分的后的表放入兩個(gè)數(shù)據(jù)庫實(shí)例。)
3)在表維度,t_order.order_id% 2 == 0 的記錄全部落到 t_order0,t_order.order_id% 2 == 1 的記錄全部落到 t_order1。
4)對記錄的讀和寫都按照這種方向進(jìn)行,“方向”,就是分片方式,就是路由。
我允許你們這些工程師使用這種簡潔的 Groovy 表達(dá)式告訴我你們設(shè)置的分片策略和分片算法。但是這種方式所能表達(dá)的含義是有限的。因此,我提供了分片策略接口和分片算法接口讓你們利用 Java 代碼盡情表達(dá)更為復(fù)雜的分片策略和分片算法。關(guān)于這一點(diǎn),我將在《我的特性和工作方法》這一章詳述。
而且在這里我要先告訴你,分片算法是分片策略的組成部分,分片策略設(shè)置=分片鍵設(shè)置+分片算法設(shè)置。上述配置里使用的策略是 Inline 類型的分片策略,使用的算法是 Inline 類型的行表達(dá)式算法,你或許不清楚我現(xiàn)在講的這些術(shù)語,不要著急,我會(huì)在《我的特性和工作方法》這一章詳述。
如果是以配置文件的方式進(jìn)行配置,示例如下(這里以我的 springboot starter 包的 properties 配置文件為例):
# 配置真實(shí)數(shù)據(jù)源
spring.shardingsphere.datasource.names=ds0,ds1
# 配置第 1 個(gè)數(shù)據(jù)源
spring.shardingsphere.datasource.ds0.type=org.apache.commons.dbcp2.BasicDataSource
spring.shardingsphere.datasource.ds0.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds0.url=jdbc:mysql://localhost:3306/ds0
spring.shardingsphere.datasource.ds0.username=root
spring.shardingsphere.datasource.ds0.password=
# 配置第 2 個(gè)數(shù)據(jù)源
spring.shardingsphere.datasource.ds1.type=org.apache.commons.dbcp2.BasicDataSource
spring.shardingsphere.datasource.ds1.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds1.url=jdbc:mysql://localhost:3306/ds1
spring.shardingsphere.datasource.ds1.username=root
spring.shardingsphere.datasource.ds1.password=
# 配置 t_order 表規(guī)則
spring.shardingsphere.rules.sharding.tables.t_order.actual-data-nodes=ds$->{0..1}.t_order$->{0..1}
# 配置 t_order 被拆分到多個(gè)子庫的策略
spring.shardingsphere.rules.sharding.tables.t_order.database-strategy.standard.sharding-column=user_id
spring.shardingsphere.rules.sharding.tables.t_order.database-strategy.standard.sharding-algorithm-name=database_inline
# 配置 t_order 被拆分到多個(gè)子表的策略
spring.shardingsphere.rules.sharding.tables.t_order.table-strategy.standard.sharding-column=order_id
spring.shardingsphere.rules.sharding.tables.t_order.table-strategy.standard.sharding-algorithm-name=table_inline
# 省略配置 t_order_item 表規(guī)則...
# ...
# 配置 t_order 被拆分到多個(gè)子庫的算法
spring.shardingsphere.rules.sharding.sharding-algorithms.database_inline.type=INLINE
spring.shardingsphere.rules.sharding.sharding-algorithms.database_inline.props.algorithm-expression=ds_${user_id % 2}
# 配置 t_order 被拆分到多個(gè)子表的算法
spring.shardingsphere.rules.sharding.sharding-algorithms.table_inline.type=INLINE
spring.shardingsphere.rules.sharding.sharding-algorithms.table_inline.props.algorithm-expression=t_order_${order_id % 2}
這段配置文件的語義和上面的 Java 配置代碼同義。
3)創(chuàng)建數(shù)據(jù)源。
若使用上文所示的 Java API 進(jìn)行配置,則可以通過 ShardingSphereDataSourceFactory 工廠創(chuàng)建數(shù)據(jù)源,該工廠產(chǎn)生一個(gè) ShardingSphereDataSource 實(shí)例,ShardingSphereDataSource 實(shí)現(xiàn)自 JDBC 的標(biāo)準(zhǔn)接口 DataSource(所以 ShardingSphereDataSource 實(shí)例也是接口 DataSource 的實(shí)例)。之后,就可以通過 dataSource 調(diào)用原生 JDBC 接口來執(zhí)行 SQL 查詢,或者將 dataSource 配置到 JPA,MyBatis 等 ORM 框架來執(zhí)行 SQL 查詢。
//?創(chuàng)建?ShardingSphereDataSource
DataSource?dataSource?=?ShardingSphereDataSourceFactory.createDataSource(
????dataSourceMap,?
????Collections.singleton(shardingRuleConfig,?new?Properties())
);
若使用上文所示的基于 springboot starter 的 properties 配置文件進(jìn)行分片配置,則可以直接通過 Spring 提供的自動(dòng)注入的方式獲得數(shù)據(jù)源實(shí)例 dataSource(同樣,這也是一個(gè) ShardingSphereDataSource 實(shí)例)。之后,就可以通過 dataSource 調(diào)用原生 JDBC 接口來執(zhí)行 SQL 查詢,或者將 dataSource 配置到 JPA,MyBatis 等 ORM 框架來執(zhí)行 SQL 查詢。
/**
*?注入一個(gè)?ShardingSphereDataSource?實(shí)例
*/
@Resource
private?DataSource?dataSource;
有了 dataSource(以上兩種方式產(chǎn)生的 dataSource 沒有區(qū)別,都是 ShardingSphereDataSource 的一個(gè)實(shí)例,業(yè)務(wù)代碼將 SQL 交給這個(gè) dataSource,也就是交到了我的手中),就可以執(zhí)行 SQL 查詢了。
4)執(zhí)行 SQL。這里給出 dataSource 調(diào)用原生 JDBC 接口來執(zhí)行 SQL 查詢的示例:
String?sql?=?"SELECT?i.*?FROM?t_order?o?JOIN?t_order_item?i?ON?o.order_id=i.order_id?WHERE?o.user_id=??AND?o.order_id=?";
try?(
????Connection?conn?=?dataSource.getConnection();
????PreparedStatement?ps?=?conn.prepareStatement(sql)
)?{
????ps.setInt(1,?10);
????ps.setInt(2,?1000);
????try?(
????????ResultSet?rs?=?preparedStatement.executeQuery()
????)?{
????????while(rs.next())?{
?????????//?...
????????}
????}
}
在這個(gè)示例中,Java 代碼調(diào)用 dataSource 的 JDBC 接口時(shí),只感覺自己在對一個(gè)邏輯庫中的兩個(gè)邏輯表進(jìn)行關(guān)聯(lián)查詢,并沒有意識(shí)到物理分片的存在。而背后是我在進(jìn)行 SQL 語句的解析、路由、改寫、執(zhí)行和結(jié)果歸并!
4. 我的特性和我的工作方法
4.2. 一些核心概念
我是 Sharding-JDBC,我是統(tǒng)治水平分片世界的神,我要向你們解釋我的特性和治理方法。在此之前,我要給出一系列用于描述我的術(shù)語。
4.2.1. 邏輯表和物理表
例如,訂單表根據(jù)主鍵尾數(shù)被水平拆分為 10 張表,分別是 t_order0 到 t_order9,它們的邏輯表名為 t_order,而 t_order0 到 t_order9 就是物理表。
4.2.2. 分片鍵
例如,若根據(jù)訂單表中的訂單主鍵的尾數(shù)取模結(jié)果進(jìn)行水平分片,則訂單主鍵為分片鍵。訂單表既可以根據(jù)單個(gè)分片鍵進(jìn)行分片,也同樣可以根據(jù)多個(gè)分片鍵(例如 order_id 和 user_id)進(jìn)行分片。
4.2.3. 路由
應(yīng)用程序服務(wù)器將針對邏輯表編寫的 SQL 交給我,我在執(zhí)行前,要找到 SQL 語句里包含的查詢條件(where ......)所對應(yīng)的分片(物理表),然后再針對這些分片進(jìn)行查詢,這個(gè)找分片的過程叫做路由。
而怎樣找分片,是由你們在分片策略中告訴我的。
4.2.4. 分片策略和分片算法
在上文的配置示例中,有如下的一段:
......
//?配置?t_order?被拆分到多個(gè)子庫的策略
orderTableRuleConfig.setDatabaseShardingStrategy(
????new?StandardShardingStrategyConfiguration(
????????"user_id",?
????????"dbShardingAlgorithm"
????)
);
//?配置?t_order?被拆分到多個(gè)子表的策略
orderTableRuleConfig.setTableShardingStrategy(
????new?StandardShardingStrategyConfiguration(
????????"order_id",?
????????"tableShardingAlgorithm"
????)
);
......
ShardingRuleConfiguration?shardingRuleConfig?=?new?ShardingRuleConfiguration();
shardingRuleConfig.getTables().add(orderTableRuleConfig);
//?配置?t_order?被拆分到多個(gè)子庫的算法
Properties?dbShardingAlgorithmrProps?=?new?Properties();
dbShardingAlgorithmrProps.setProperty(
????"algorithm-expression",?
????"ds${user_id?%?2}"
);
shardingRuleConfig.getShardingAlgorithms().put(
????"dbShardingAlgorithm",?
????new?ShardingSphereAlgorithmConfiguration("INLINE",?dbShardingAlgorithmrProps)
);
//?配置?t_order?被拆分到多個(gè)子表的算法
Properties?tableShardingAlgorithmrProps?=?new?Properties();
tableShardingAlgorithmrProps.setProperty(
????"algorithm-expression",?
????"t_order${order_id?%?2}"
);
shardingRuleConfig.getShardingAlgorithms().put(
????"tableShardingAlgorithm",?
????new?ShardingSphereAlgorithmConfiguration("INLINE",?tableShardingAlgorithmrProps)
);
......
它們表達(dá)的就是對 t_order 表進(jìn)行的分片策略和分片算法的配置。
上文說到,我允許你們這些工程師使用簡潔的 Groovy 表達(dá)式告訴我你們設(shè)置的分片策略和分片算法。但是這種方式所能表達(dá)的含義是有限的。因此,我提供了分片策略接口和分片算法接口讓你們利用靈活的 Java 代碼盡情表達(dá)更為復(fù)雜的分片策略和分片算法。
所謂分片策略,就是分片鍵和分片算法的組合,由于分片算法的獨(dú)立性,我將其獨(dú)立抽離出來,由你們自己實(shí)現(xiàn),也就是告訴我數(shù)據(jù)是怎么根據(jù)分片鍵的值找到對應(yīng)的分片,進(jìn)而對這些分片執(zhí)行 SQL 查詢。
當(dāng)然我也提供了一些內(nèi)置的簡單算法的實(shí)現(xiàn)。上面提到的基于 Groovy 表達(dá)式的分片算法就是我內(nèi)置的一種算法實(shí)現(xiàn),你們只要給我一段語義準(zhǔn)確無誤的 Groovy 表達(dá)式,我就能知道怎么根據(jù)分片鍵的值找到對應(yīng)的分片。
我的分片策略有兩個(gè)維度,如下圖所示,分別是數(shù)據(jù)源分片策略(databaseShardingStrategy)和表分片策略(tableShardingStrategy)。數(shù)據(jù)源分片策略表示數(shù)據(jù)被路由到目標(biāo)物理數(shù)據(jù)庫的策略,表分片策略表示數(shù)據(jù)被路由到目標(biāo)物理表的策略。表分片策略是依賴于數(shù)據(jù)源分片策略的,也就是說要先分庫再分表,當(dāng)然也可以只分表。

我目前可以提供如下幾種分片(無論是對庫分片還是對表分片)策略:標(biāo)準(zhǔn)分片策略(使用精確分片算法或者范圍分片算法)、復(fù)合分片策略(使用符合分片算法)、Hint 分片策略(使用 Hint 分片算法)、Inline 分片策略(使用 Grovvy 表達(dá)式作為分片算法)、不分片策略(不使用分片算法)。
我的 Jar 包源碼里的策略類和算法接口如下:


一、標(biāo)準(zhǔn)分片策略
標(biāo)準(zhǔn)分片策略 StandardShardingStrategy 的源代碼(部分)如下,這是一個(gè) final class。
package?org.apache.shardingsphere.core.strategy.route.standard;
......
public?final?class?StandardShardingStrategy?implements?ShardingStrategy?{
????
????private?final?String?shardingColumn;
????
????/**
????*?要配合?PreciseShardingAlgorithm?或?RangeShardingAlgorithm?使用
????*?標(biāo)準(zhǔn)分片策略
????*/
????private?final?PreciseShardingAlgorithm?preciseShardingAlgorithm;
????
????private?final?RangeShardingAlgorithm?rangeShardingAlgorithm;
????
????public?StandardShardingStrategy(
????????//?傳入分片配置
????????final?StandardShardingStrategyConfiguration?standardShardingStrategyConfig
????)?{
????????......
????????
????????//?從配置中提取分片鍵
????????shardingColumn?=?standardShardingStrategyConfig.getShardingColumn();
????????//?從配置中提取分片算法
????????preciseShardingAlgorithm?=?standardShardingStrategyConfig.getPreciseShardingAlgorithm();
????????rangeShardingAlgorithm?=?standardShardingStrategyConfig.getRangeShardingAlgorithm();
????}
????
????@Override
????public?Collection?doSharding(
????????//?所有可能的分片表(或分片庫)名稱
????????final?Collection?availableTargetNames,?
????????//?分片鍵的值
????????final?Collection?shardingValues,?
????????final?ConfigurationProperties?properties
????)? {
????????RouteValue?shardingValue?=?shardingValues.iterator().next();
????????Collection?shardingResult?
????????????=?shardingValue?instanceof?ListRouteValue
????????????????//?處理精確分片
??????????????????doSharding(availableTargetNames,?(ListRouteValue)?shardingValue)?
????????????????//?處理范圍分片
????????????????:?doSharding(availableTargetNames,?(RangeRouteValue)?shardingValue);
????????Collection?result?=?new?TreeSet<>(String.CASE_INSENSITIVE_ORDER);
????????result.addAll(shardingResult);
??
????????//?根據(jù)分片鍵的值,找到對應(yīng)的分片表(或分片庫)名稱并返回
????????return?result;
????}
????
????/**
????*?處理范圍分片
????*/
????@SuppressWarnings("unchecked")
????private?Collection?doSharding(
????????//?所有可能的分片表(或分片庫)名稱
????????final?Collection?availableTargetNames,?
????????//?分片鍵的值
????????final?RangeRouteValue>?shardingValue
????)? {
????????......
????????//?調(diào)用?rangeShardingAlgorithm.doSharding()根據(jù)分片鍵的值找到對應(yīng)的
????????//?分片表(或分片庫)名稱并返回,rangeShardingAlgorithm.doSharding()
????????//?由你們自己實(shí)現(xiàn)
????????return?rangeShardingAlgorithm.doSharding(
????????????availableTargetNames,?
????????????new?RangeShardingValue(
????????????????shardingValue.getTableName(),?
????????????????shardingValue.getColumnName(),?
????????????????shardingValue.getValueRange()
????????????)
????????);
????}
????
????/**
????*?處理精確分片
????*/
????@SuppressWarnings("unchecked")
????private?Collection?doSharding(
????????//?所有可能的分片表(或分片庫)名稱
????????final?Collection?availableTargetNames,?
????????//?分片鍵的值
????????final?ListRouteValue>?shardingValue
????)? {
????????Collection?result?=?new?LinkedList<>();
????????for?(Comparable>?each?:?shardingValue.getValues())?{
????????????//?調(diào)用?preciseShardingAlgorithm.doSharding()根據(jù)分片鍵的值找到對應(yīng)的
????????????//?分片表(或分片庫)名稱并返回,preciseShardingAlgorithm.doSharding()
????????????//?由你們自己實(shí)現(xiàn)
????????????String?target?
????????????????=?preciseShardingAlgorithm.doSharding(
????????????????availableTargetNames,?
????????????????new?PreciseShardingValue(
????????????????????shardingValue.getTableName(),?
????????????????????shardingValue.getColumnName(),?
????????????????????each
????????????????)
????????????);
????????????if?(null?!=?target)?{
????????????????result.add(target);
????????????}
????????}
????????return?result;
????}
????
????/**
????*?獲取所有的分片鍵
????*/
????@Override
????public?Collection?getShardingColumns()? {
????????Collection?result?=?new?TreeSet<>(String.CASE_INSENSITIVE_ORDER);
????????result.add(shardingColumn);
????????return?result;
????}
}
其中 PreciseShardingAlgorithm(接口)和 RangeShardingAlgorithm(接口)的源代碼分別為:
package?org.apache.shardingsphere.api.sharding.standard;
......
public?interface?PreciseShardingAlgorithm<T?extends?Comparable>>?
????extends?ShardingAlgorithm?{
????
????/**
?????*?@param?所有可能的分片表(或分片庫)名稱
?????*?@param?分片鍵的值
?????*?@return?根據(jù)分片鍵的值,找到對應(yīng)的分片表(或分片庫)名稱并返回
?????*/
????String?doSharding(
????????Collection?availableTargetNames,?
????????PreciseShardingValue?shardingValue
????) ;
}
package?org.apache.shardingsphere.api.sharding.standard;
......
public?interface?RangeShardingAlgorithm<T?extends?Comparable>>?
????extends?ShardingAlgorithm?{
????
????/**
?????*?@param?所有可能的分片表(或分片庫)名稱
?????*?@param?分片鍵的值
?????*?@return?根據(jù)分片鍵的值,找到對應(yīng)的分片表(或分片庫)名稱并返回
?????*/
????Collection?doSharding(
????????Collection?availableTargetNames,?
????????RangeShardingValue?shardingValue
????) ;
}
標(biāo)準(zhǔn)分片策略提供對 SQL 語句中的操作符 =、>、 <、>=、<=、IN 和 BETWEEN AND 的分片操支持。
標(biāo)準(zhǔn)分片策略只支持單分片鍵,例如對 t_order 表只根據(jù) order_id 分片。標(biāo)準(zhǔn)分片策略提供 PreciseShardingAlgorithm(接口)和 RangeShardingAlgorithm(接口)兩個(gè)分片算法。PreciseShardingAlgorithm(接口)顧名思義用于處理操作符=和 IN 的精確分片。RangeShardingAlgorithm (接口)顧名思義用于處理操作符 BETWEEN AND、>、<、>=、<= 的范圍分片。
我舉個(gè)例子幫助你理解以上兩段話的含義。以 t_order 為例,假如你使用 order_id 作為 t_order 的分片鍵,并設(shè)計(jì)了以下的分片策略:
策略一:設(shè)置 6 個(gè)分片
t_order.order_id?%?6?==?0?的查詢分片到?t_order0
t_order.order_id?%?6?==?1?的查詢分片到?t_order1
t_order.order_id?%?6?==?2?的查詢分片到?t_order2
t_order.order_id?%?6?==?3?的查詢分片到?t_order3
t_order.order_id?%?6?==?4?的查詢分片到?t_order4
t_order.order_id?%?6?==?5?的查詢分片到?t_order5
策略二:設(shè)置 2 個(gè)分片
t_order.order_id?%?6?in?(0,2,4)?的查詢分片到?t_order1
t_order.order_id?%?6?in?(1,3,5)?的查詢分片到?t_order1
策略三:經(jīng)過估算訂單不超過 60000?個(gè),設(shè)置 6 個(gè)分片
t_order.order_id?between?0?and?10000?的查詢分片到?t_order0
t_order.order_id?between?10000?and?20000?的查詢分片到?t_order1
t_order.order_id?between?20000?and?30000?的查詢分片到?t_order2
t_order.order_id?between?30000?and?40000?的查詢分片到?t_order3
t_order.order_id?between?40000?and?50000?的查詢分片到?t_order4
t_order.order_id?between?50000?and?60000?的查詢分片到?t_order5
策略四:經(jīng)過估算訂單不超過 20000?個(gè),設(shè)置 2 個(gè)分片
t_order.order_id?<=10000?的查詢分片到?t_order0
t_order.order_id?>10000?的查詢分片到?t_order1
......
那你就可以把以下三項(xiàng):
1)分片鍵 order_id
2)描述以上分片策略內(nèi)容的 PreciseShardingAlgorithm(接口)的實(shí)現(xiàn)類或 RangeShardingAlgorithm(接口)的實(shí)現(xiàn)類
3)前兩項(xiàng)(即分片策略)的作用目標(biāo) t_order 表
寫到分片配置里(無論是通過配置 API 還是通過配置文件),那我就能知道如何去路由 SQL,即根據(jù)分片鍵的值,找到對應(yīng)的分片表(或分片庫)。
有了這些配置,我就能幫你們透明處理如下 SQL 語句,不管實(shí)際的物理分片是怎樣的:
--?注:使用 t_order.order_id 作為 t_order 表的分片鍵
SELECT?o.*?FROM?t_order?o?WHERE?o.order_id?=?10;
SELECT?o.*?FROM?t_order?o?WHERE?o.order_id?IN?(10,?11);
SELECT?o.*?FROM?t_order?o?WHERE?o.order_id?>?10;
SELECT?o.*?FROM?t_order?o?WHERE?o.order_id?<=?11;
SELECT?o.*?FROM?t_order?o?WHERE?o.order_id?BETWEEN?10?AND?12;
......
INSERT?INTO?t_order(order_id,?user_id)?VALUES?(20,?1001);
......
DELETE?FROM?t_order?o?WHERE?o.order_id?=?10;
DELETE?FROM?t_order?o?WHERE?o.order_id?IN?(10,?11);
DELETE?FROM?t_order?o?WHERE?o.order_id?>?10;
DELETE?FROM?t_order?o?WHERE?o.order_id?<=?11;
DELETE?FROM?t_order?o?WHERE?o.order_id?BETWEEN?10?AND?12;
......
UPDATE?t_order?o?SET?o.update_time?=?NOW()?WHERE?o.order_id?=?10;
......
二、復(fù)合分片策略
復(fù)合分片策略 ComplexShardingStrategy 的源代碼(部分)如下,這是一個(gè) final class。
package?org.apache.shardingsphere.core.strategy.route.complex;
......
public?final?class?ComplexShardingStrategy?implements?ShardingStrategy?{
????
????@Getter
????private?final?Collection?shardingColumns;
????
????/**
????*?要配合?ComplexKeysShardingAlgorithm?使用復(fù)合分片策略
????*/
????private?final?ComplexKeysShardingAlgorithm?shardingAlgorithm;
????
????public?ComplexShardingStrategy(
????????//?傳入分片配置
????????final?ComplexShardingStrategyConfiguration?complexShardingStrategyConfig
????)?{
????????......
????????//?從配置中提取分片鍵
????????shardingColumns?=?new?TreeSet<>(String.CASE_INSENSITIVE_ORDER);
????????shardingColumns.addAll(
????????????Splitter
????????????.on(",")
????????????.trimResults()
????????????.splitToList(complexShardingStrategyConfig.getShardingColumns())
????????);
????????//?從配置中提取分片算法
????????shardingAlgorithm?=?complexShardingStrategyConfig.getShardingAlgorithm();
????}
????
????@SuppressWarnings("unchecked")
????@Override
????public?Collection?doSharding(
????????//?所有可能的分片表(或分片庫)名稱
????????final?Collection?availableTargetNames,?
????????//?分片鍵的值
????????final?Collection?shardingValues,?
????????final?ConfigurationProperties?properties
????)? {
????????Map>>?columnShardingValues?
????????????=?new?HashMap<>(shardingValues.size(),?1);
????????Map>>?columnRangeValues?
????????????=?new?HashMap<>(shardingValues.size(),?1);
????????String?logicTableName?=?"";
????????
????????//?提取多個(gè)分片鍵的值
????????for?(RouteValue?each?:?shardingValues)?{
????????????if?(each?instanceof?ListRouteValue)?{
????????????????columnShardingValues.put(
????????????????????each.getColumnName(),?
????????????????????((ListRouteValue)?each).getValues()
????????????????);
????????????}?else?if?(each?instanceof?RangeRouteValue)?{
????????????????columnRangeValues.put(
????????????????????each.getColumnName(),?
????????????????????((RangeRouteValue)?each).getValueRange()
????????????????);
????????????}
????????????logicTableName?=?each.getTableName();
????????}
????????Collection?shardingResult?
????????????//?調(diào)用?shardingAlgorithm.doSharding()根據(jù)分片鍵的值找到對應(yīng)的
????????????//?分片表(或分片庫)名稱并返回,shardingAlgorithm.doSharding()
????????????//?由你們自己實(shí)現(xiàn)
????????????=?shardingAlgorithm.doSharding(
????????????availableTargetNames,?
????????????new?ComplexKeysShardingValue(
????????????????logicTableName,?
????????????????columnShardingValues,?
????????????????columnRangeValues)
????????);
????????Collection?result?=?new?TreeSet<>(String.CASE_INSENSITIVE_ORDER);
????????result.addAll(shardingResult);
????????
????????//?根據(jù)分片鍵的值,找到對應(yīng)的分片表(或分片庫)名稱并返回
????????return?result;
????}
}
其中 ComplexKeysShardingAlgorithm(接口)的源代碼為:
package?org.apache.shardingsphere.api.sharding.complex;
......
public?interface?ComplexKeysShardingAlgorithm<T?extends?Comparable>>?
????extends?ShardingAlgorithm?{
????
????/**
?????*?@param?所有可能的分片表(或分片庫)名稱
?????*?@param?分片鍵的值
?????*?@return?根據(jù)分片鍵的值,找到對應(yīng)的分片表(或分片庫)名稱并返回
?????*/
????Collection?doSharding(
????????Collection?availableTargetNames,?
????????ComplexKeysShardingValue?shardingValue
????) ;
}
復(fù)合分片策略提供對 SQL 語句中的操作符 =、>、<、>=、<=、IN 和 ETWEEN AND 的分片操作支持。
復(fù)合分片策略支持多分片鍵,例如對 t_order 表根據(jù) order_id 和 user_id 分片。復(fù)合分片策略提供 ComplexKeysShardingAlgorithm(接口)分片算法。
我舉個(gè)例子幫助你理解以上兩段話的含義。以 t_order 為例,假如你使用 order_id 和 user_id 作為 t_order 的分片鍵,并設(shè)計(jì)了以下的分片策略:
策略一:設(shè)置 4 個(gè)分片
t_order.order_id?%?2?==?0?&&?t_order.user_id?%?2?==?0?的查詢分片到?t_order0
t_order.order_id?%?2?==?0?&&?t_order.user_id?%?2?==?1?的查詢分片到?t_order1
t_order.order_id?%?2?==?1?&&?t_order.user_id?%?2?==?0?的查詢分片到?t_order2
t_order.order_id?%?2?==?1?&&?t_order.user_id?%?2?==?1?的查詢分片到?t_order3
策略二:經(jīng)過估算訂單不超過 60000?個(gè)、用戶不超過 1000?個(gè),設(shè)置 4 個(gè)分片
t_order.order_id?between?0?and?40000?&&?t_order.user_id?between?0?and?500?的查詢分片到?t_order0
t_order.order_id?between?0?and?40000?&&?t_order.user_id?between?500?and?1000?的查詢分片到?t_order1
t_order.order_id?between?40000?and?60000?&&?t_order.user_id?between?0?and?500?的查詢分片到?t_order2
t_order.order_id?between?40000?and?60000?&&?t_order.user_id?between?500?and?1000?的查詢分片到?t_order3
......
那你就可以把以下三項(xiàng):
1)分片鍵 order_id 和 user_id
2)描述以上分片策略內(nèi)容的 ComplexKeysShardingAlgorithm(接口)的實(shí)現(xiàn)類
3)前兩項(xiàng)(即分片策略)的作用目標(biāo) t_order 表
寫到分片配置里(無論是通過配置 API 還是通過配置文件),那我就能知道如何去路由 SQL,即根據(jù)分片鍵的值,找到對應(yīng)的分片表(或分片庫)。
有了這些配置,我就能幫你們透明處理如下 SQL 語句,不管實(shí)際的物理分片是怎樣的:
--?注:使用 t_order.order_id、t_order.user_id 作為 t_order 表的分片鍵
SELECT?o.*?FROM?t_order?o?WHERE?o.order_id?=?10;
SELECT?o.*?FROM?t_order?o?WHERE?o.order_id?IN?(10,?11);
SELECT?o.*?FROM?t_order?o?WHERE?o.order_id?>?10;
SELECT?o.*?FROM?t_order?o?WHERE?o.order_id?<=?11;
SELECT?o.*?FROM?t_order?o?WHERE?o.order_id?BETWEEN?10?AND?12;
......
INSERT?INTO?t_order(order_id,?user_id)?VALUES?(20,?1001);
......
DELETE?FROM?t_order?o?WHERE?o.order_id?=?10;
DELETE?FROM?t_order?o?WHERE?o.order_id?IN?(10,?11);
DELETE?FROM?t_order?o?WHERE?o.order_id?>?10;
DELETE?FROM?t_order?o?WHERE?o.order_id?<=?11;
DELETE?FROM?t_order?o?WHERE?o.order_id?BETWEEN?10?AND?12;
......
UPDATE?t_order?o?SET?o.update_time?=?NOW()?WHERE?o.order_id?=?10;
......
SELECT?o.*?FROM?t_order?o?WHERE?o.order_id?=?10?AND?user_id?=?1001;
SELECT?o.*?FROM?t_order?o?WHERE?o.order_id?IN?(10,?11)?AND?user_id?IN?(......);
SELECT?o.*?FROM?t_order?o?WHERE?o.order_id?>?10?AND?user_id?>?1000;
SELECT?o.*?FROM?t_order?o?WHERE?o.order_id?<=?11?AND?user_id?<=?1000;
SELECT?o.*?FROM?t_order?o?WHERE?(o.order_id?BETWEEN?10?AND?12)?AND?(o.user_id?BETWEEN?1000?AND?2000);
......
INSERT?INTO?t_order(order_id,?user_id)?VALUES?(21,?1002);
......
DELETE?FROM?t_order?o?WHERE?o.order_id?=?10?AND?user_id?=?1001;
DELETE?FROM?t_order?o?WHERE?o.order_id?IN?(10,?11)?AND?user_id?IN?(......);
DELETE?FROM?t_order?o?WHERE?o.order_id?>?10?AND?user_id?>?1000;
DELETE?FROM?t_order?o?WHERE?o.order_id?<=?11?AND?user_id?<=?1000;
DELETE?FROM?t_order?o?WHERE?(o.order_id?BETWEEN?10?AND?12)?AND?(o.user_id?BETWEEN?1000?AND?2000);
......
UPDATE?t_order?o?SET?o.update_time?=?NOW()?WHERE?o.order_id?=?10?AND?user_id?=?1001;
......
注:在《召喚我的方式》這一章,我給出了一段配置,這段配置表明先依照 user_id % 2 對 t_order 進(jìn)行水平拆分(到不同的子庫),再依照 order_id % 2 對 t_order 進(jìn)行水平拆分(到不同的子表)。但這并不是說使用了復(fù)合分片策略,而是使用了兩個(gè)兩個(gè)維度的標(biāo)準(zhǔn)分片策略。兩個(gè)維度,分別是數(shù)據(jù)源分片策略(DatabaseShardingStrategy)和表分片策略(TableShardingStrategy),且在數(shù)據(jù)源分片策略上使用 user_id 作為單分片鍵、在表分片策略上使用 order_id 作為單分片鍵。
三、Hint(翻譯為暗示) 分片策略
Hint 分片策略對應(yīng) HintShardingStrategy 這個(gè) final class,同標(biāo)準(zhǔn)分片策略和符合分片策略的代碼類似,HintShardingStrategy 中包含一個(gè) HintShardingAlgorithm 接口的實(shí)例,并調(diào)用它的 doSharding()方法。你們要自己去實(shí)現(xiàn)這個(gè) HintShardingAlgorithm 接口中的 doSharding()方法,這樣我就能知道如何根據(jù)分片鍵的值,找到對應(yīng)的分片表(或分片庫)。此處不在展示 HintShardingStrategy 和 HintShardingAlgorithm 的源碼。
Hint 分片策略是指通過 Hint 指定分片值而非從 SQL 中提取分片值的方式進(jìn)行分片的策略。簡單來講就是我收到的 SQL 語句中不包含分片值(像上面給出的幾段 SQL 就是包含分片值的 SQL),但是工程師會(huì)通過我提供的 Java API 將分片值暗示給我,這樣我就知道怎樣路由 SQL 查詢到具體的分片了。就像下面這樣:
String?sql?=?"SELECT?*?FROM?t_order";
try?(
????//?HintManager?是使用“暗示”的工具,它會(huì)把暗示的分片值放入
????//?當(dāng)前線程上下文(ThreadLocal)中,這樣當(dāng)前線程執(zhí)行?SQL?的
????//?時(shí)候就能獲取到分片值
????HintManager?hintManager?=?HintManager.getInstance();
????Connection?conn?=?dataSource.getConnection();
????PreparedStatement?pstmt?=?conn.prepareStatement(sql);
)?{
????hintManager.setDatabaseShardingValue(3);
????try?(ResultSet?rs?=?pstmt.executeQuery())?{
????????//?若?t_order?僅僅使用?order_id?作為分片鍵,則這里根據(jù)暗
????????//?示獲取了分片值,因此上面的 SQL 的實(shí)際執(zhí)行效果相當(dāng)于:
????????//?SELECT?*?FROM?t_order?where?order_id?=?3
????????while?(rs.next())?{
?????????//...
????????}?
????}?
}
四、不分片策略
對應(yīng) NoneShardingStrategy,這是一個(gè) final class。由于我并不要求所有的表(或庫)都進(jìn)行水平分片,因此當(dāng)工程師要通過我執(zhí)行對不分片表(或庫)的 SQL 查詢時(shí),就要使用這個(gè)不分片策略。NoneShardingStrategy 的源碼為:
package?org.apache.shardingsphere.core.strategy.route.none;
......
@Getter
public?final?class?NoneShardingStrategy?implements?ShardingStrategy?{
????
????private?final?Collection?shardingColumns?=?Collections.emptyList();
????
????@Override
????public?Collection?doSharding(
????????//?所有可能的分片表(或分片庫)名稱
????????final?Collection?availableTargetNames,?
????????//?分片鍵的值
????????final?Collection?shardingValues,?
????????final?ConfigurationProperties?properties
????)? {
????????
????????//?不需要任何算法,不進(jìn)行任何邏輯處理,直接返回
????????//?所有可能的分片表(或分片庫)名稱
????????return?availableTargetNames;
????}
}
五、Inline 分片策略
Inline 分片策略,也叫做行表達(dá)式分片策略。Inline 分片策略對應(yīng) InlineShardingStrategy。Inline 分片策略是為用 Grovvy 表達(dá)式描述的分片算法準(zhǔn)備的分片策略。文章開始展示的兩段配置中就使用了 Inline 分片策略。InlineShardingStrategy 把 Grovvy 表達(dá)式當(dāng)做分片算法的實(shí)現(xiàn),因此 HintShardingStrategy 中不包含算法域變量,這一點(diǎn)有別于 StandardShardingStrategy 等 final class。這里不再展示 InlineShardingStrategy 的源碼。
我知道,這段關(guān)于分片策略和分片算法的表述很難理解。不過我還是想讓你們明白,無論對某個(gè)邏輯表(或庫)進(jìn)行怎樣的分片策略配置,這些策略不過都是在告訴我怎樣處理分片,也就是告訴我如何根據(jù)分片鍵的值,找到對應(yīng)的分片表(或分片庫)。只不過我的創(chuàng)造者把這個(gè)簡單的過程翻出了很多花樣,也就是你們在上面看到的各種策略,以提供使用上的靈活性。
4.2.5. 綁定表
指分片規(guī)則一致的主表和子表。例如 t_order 是主表,存儲(chǔ)訂單的基本信息;t_order_item 是子表,存儲(chǔ)訂單中的商品和價(jià)格明細(xì)。若兩張表均按照 order_id 分片,并且配置了兩個(gè)表之間的綁定關(guān)系,則此兩張表互為綁定表。綁定表之間的多表關(guān)聯(lián)查詢不會(huì)出現(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í),假設(shè)分片鍵 order_id 將數(shù)值 10 路由至第 0 片,將數(shù)值 11 路由至第 1 片,那么路由后的 SQL 應(yīng)該為 4 條,它們呈現(xiàn)為笛卡爾積,這種情況是我最不愿意處理的,我要考慮所有可能的分組合,它的工作量實(shí)在太大了:
SELECT?i.*?FROM?t_order0?o?JOIN?t_order_item0?i?ON?o.order_id=i.order_id?WHERE?o.order_id?IN?(10,?11);
SELECT?i.*?FROM?t_order0?o?JOIN?t_order_item1?i?ON?o.order_id=i.order_id?WHERE?o.order_id?IN?(10,?11);
SELECT?i.*?FROM?t_order1?o?JOIN?t_order_item0?i?ON?o.order_id=i.order_id?WHERE?o.order_id?IN?(10,?11);
SELECT?i.*?FROM?t_order1?o?JOIN?t_order_item1?i?ON?o.order_id=i.order_id?WHERE?o.order_id?IN?(10,?11);
而在配置綁定表關(guān)系后,路由的 SQL 只有 2 條:
SELECT?i.*?FROM?t_order0?o?JOIN?t_order_item0?i?ON?o.order_id=i.order_id?WHERE?o.order_id?IN?(10,?11);
SELECT?i.*?FROM?t_order1?o?JOIN?t_order_item1?i?ON?o.order_id=i.order_id?WHERE?o.order_id?IN?(10,?11);
而我也提供了這種綁定關(guān)系配置的 API 和配置項(xiàng),例如在 properties 配置文件中可以這么寫:
# 設(shè)置綁定表
sharding.jdbc.config.sharding.binding-tables=t_order, t_order_item
4.3. 我處理 SQL 的過程
我是 Sharding-JDBC,我是水平分片世界的神。我的職責(zé)是透明化水平分庫分表所帶來的影響,讓使用方盡量像使用一個(gè)數(shù)據(jù)庫一樣使用水平分片之后的數(shù)據(jù)庫集群,或者像使用一個(gè)數(shù)據(jù)表一樣使用水平分片之后的數(shù)據(jù)表。
我的法力,來源于我的創(chuàng)造者為我設(shè)計(jì)的內(nèi)核,它把 SQL 語句的處理分成了 SQL 解析 =>SQL 路由 => SQL 改寫 => SQL 執(zhí)行 => 結(jié)果歸并五個(gè)主要流程。

當(dāng)一個(gè)應(yīng)用服務(wù)器節(jié)點(diǎn)將一個(gè)面向邏輯表編寫的查詢 SQL 交給我之后,我要做下面幾件事:
1)SQL 解析(由我內(nèi)核中的解析引擎完成):先通過詞法解析器將邏輯 SQL 拆分為一個(gè)個(gè)不可再分的單詞,再使用語法解析器對 SQL 進(jìn)行理解,并最終提煉出解析上下文。
2)SQL 路由(由我內(nèi)核中的路由引擎完成):根據(jù)解析上下文匹配用戶配置的分片策略(關(guān)于用戶配置的分片策略,我后文會(huì)慢慢解釋),并生成路由路徑,路由路徑指示了 SQL 最終要到哪些分片去執(zhí)行。
3)SQL 改寫(由我內(nèi)核中的改寫引擎完成):將 面向邏輯表 SQL 改寫為在真實(shí)數(shù)據(jù)庫中可以正確執(zhí)行的語句(邏輯 SQL 到物理 SQL 的映射)。
4)SQL 執(zhí)行(由我內(nèi)核中的執(zhí)行引擎完成):通過多線程執(zhí)行器異步執(zhí)行路由和改寫之后得到的 SQL 語句。
5)結(jié)果歸并(由我內(nèi)核中的歸并引擎完成):將多個(gè)執(zhí)行結(jié)果集歸并以便于通過統(tǒng)一的 JDBC 接口輸出。
4.3.1. SQL 解析
SQL 解析 SQL 解析分為詞法解析和語法解析。
我的解析引擎先通過詞法解析器將這句 SQL 拆分為一個(gè)個(gè)不可再分的單詞,再使用語法解析器對 SQL 進(jìn)行理解,并最終提煉出解析上下文。解析上下文包括表、選擇項(xiàng)、排序項(xiàng)、分組項(xiàng)、聚合函數(shù)、分頁信息、查詢條件以及可能需要修改的占位符的標(biāo)記。簡單來說就是我要理解這句 SQL,明白它的結(jié)構(gòu)和意圖。所幸,SQL 是一個(gè)語法簡單的語言,SQL 解析這件事情并不復(fù)雜。
我先使用解析引擎的詞法解析器用于將 SQL 拆解為不可再分的原子符號(hào),我把它們叫做 Token,并將其歸類為關(guān)鍵字、表達(dá)式、字面量、操作符,再使用解析引擎的語法解析器將 SQL 轉(zhuǎn)換為抽象語法樹。
例如,以下 SQL:
SELECT?id,?name?FROM?t_user?WHERE?status?=?'ACTIVE'?AND?age?>?18
被我的詞法解析器和語法解析器解析之后得到的抽象語法樹為:

在上圖中,為了便于理解,抽象語法樹中的關(guān)鍵字和操作符的 Token 用綠?表示,字面量的 Token 用紅?表示,灰?表示需要進(jìn)一步拆分。
最后,我通過對抽象語法樹的遍歷去提煉分片所需的上下文,并標(biāo)記有可能需要改寫的位置。供分片使用的解析上下文包含查詢選擇項(xiàng)(Select Items)、表信息(Table)、分片條件(Sharding Condition)、自增主鍵信息(Auto increment Primary Key)、排序信息(Order By)、分組信息(Group By)以及分頁信息(Limit、Rownum、Top)。
SQL 解析是下面的路由、改寫、執(zhí)行和歸并的基礎(chǔ)。
4.3.2. SQL 路由
我的內(nèi)核在這一階段根據(jù) SQL 的解析上下文匹配數(shù)據(jù)庫和表的分片策略(還記得嗎,我在《一些核心概念》這一節(jié)說過,分片策略=分片鍵+分片算法,分片策略會(huì)指示我如何根據(jù)分片鍵的值,找到對應(yīng)的分片表或分片庫),找到對應(yīng)的分片表或分片庫,并生成路由后的 SQL。
對于攜帶分片鍵的 SQL,我會(huì)根據(jù)分片鍵值的不同可以劃分為單片路由 (比如分片鍵的操作符是=)、多片路由 (比如分片鍵的操作符是 IN、BETWEEN AND、>、<、>=、<=,或者多表關(guān)聯(lián)查詢)。單片路由生成針對某個(gè)分片進(jìn)行查詢的 SQL,多片路由生成針對某些分片進(jìn)行查詢的 SQL。
不攜帶分片鍵的 SQL 則采用全路由(全路由是一種特殊的多片路由),即生成針對所有分片進(jìn)行查詢的 SQL。但如果這條 SQL 能夠匹配 Hint 分片策略,我就知道工程師會(huì)通過我的 API 把分片鍵值暗示給我,這時(shí)候我從 API 拿到分片鍵值后也會(huì)去做單片或者多片路由。
這里的單片路由、多片路由或者全庫路由是對路由劃分的一種角度,它反映了我最終執(zhí)行 SQL 的路徑有幾條:若 SQL 解析上下文最終被計(jì)算出存在單片路由,在一個(gè)數(shù)據(jù)源內(nèi)我只需要針對一個(gè)分片上去執(zhí)行 SQL;若 SQL 解析上下文最終被計(jì)算出存在多片路由,在一個(gè)數(shù)據(jù)源內(nèi)我需要針對多個(gè)分片上去執(zhí)行 SQL。若 SQL 解析上下文最終被計(jì)算出存在全路由,在一個(gè)數(shù)據(jù)源內(nèi)我就要針對全部分片去執(zhí)行 SQL。
下面是一些實(shí)例:
--?若僅以 user_id 作為分片鍵對 t_user 進(jìn)行分片,且分片算法為 user_id % 5,則以下 SQL 在一個(gè)數(shù)據(jù)源內(nèi)會(huì)針對一個(gè)特定分片執(zhí)行:
SELECT?*?FROM?t_user?WHERE?user_id?=?1009?--路由到?t_user4?執(zhí)行
--?若僅以 user_id 作為分片鍵對 t_user 進(jìn)行分片,且分片算法為 user_id % 5,則以下 SQL 在一個(gè)數(shù)據(jù)源內(nèi)會(huì)針對多個(gè)分片執(zhí)行:
SELECT?*?FROM?t_user?WHERE?user_id?in?(1002,?1003,?1009)?--路由到?t_user2、t_user3、t_user4
SELECT?*?FROM?t_user?WHERE?user_id?>?1002?AND?user_id?<=?1004?--路由到?t_user3、t_user4
SELECT?*?FROM?t_user?WHERE?user_id?between?1002?and?1004?--路由到?t_user2、t_user3
--?若僅以 user_id 作為分片鍵對 t_user 進(jìn)行分片,且分片算法為 user_id % 5,則以下 SQL 在一個(gè)數(shù)據(jù)源內(nèi)會(huì)針對所有的分片執(zhí)行:
SELECT?count(1)?FROM?t_user?--路由到?t_user0、t_user1、t_user2、t_user3、t_user4
SELECT?*?FROM?t_user?where?age?18?--路由到?t_user0、t_user1、t_user2、t_user3、t_user4
4.3.3. SQL 改寫
?程師交給我處理的 SQL 是面向邏輯表書寫的 SQL,并不能夠直接在數(shù)據(jù)庫中執(zhí)行,所以我的內(nèi)核要完成 SQL 改寫,將面向邏輯表的 SQL 改寫面向物理表的 SQL。SQL 改寫分為標(biāo)識(shí)符改寫、補(bǔ)列、分頁修正、批量拆分。

一、標(biāo)識(shí)符改寫
在水平分片的場景中,需要將 SQL 中的邏輯表名改寫為路由之后所對應(yīng)的物理分片表名,索引名稱以及 Schema 名稱也要進(jìn)行邏輯名到物理名的改寫。
1)表名稱改寫
表名稱改寫是指將找到邏輯表名在原始 SQL 中的位置,并將其改寫為真實(shí)分片表名的過程。比如,若邏輯 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_order1?WHERE?order_id=1;
你或許會(huì)以為只要通過字符串查找和替換就可以達(dá)到 SQL 改寫的效果,但事實(shí)并非如此,例如:
SELECT?t_order.order_id?FROM?t_order?AS?t_order?WHERE?t_order.order_id=1?AND?remarks='備注?t_order?xxx';
SQL 改寫則僅需要改寫表名稱就可以了,別名“t_order”、備注字段內(nèi)容“t_order”均無需改寫:
SELECT?t_order.order_id?FROM?t_order_1?AS?t_order?WHERE?t_order.order_id=1?AND?remarks='備注?t_order?xxx';
因此表名稱改寫是一個(gè)典型的需要對 SQL 進(jìn)行詞法和語法解析的場景,它依賴于 SQL 解析上下文,即依賴于對 SQL 語義的理解,而不是簡單的字符串替換!對于包含索引和 Schema 的 SQL 改寫也是一樣。
2)索引名稱改寫
索引名稱是另一個(gè)有可能改寫的標(biāo)識(shí)符。在某些數(shù)據(jù)庫中(如 MySQL、SQLServer),索引是以表為維度創(chuàng)建的,在不同的表中的索引是可以重名的;而在另外的一些數(shù)據(jù)庫中(如 PostgreSQL、Oracle),索引是以數(shù)據(jù)庫為維度創(chuàng)建的,即使是作用在不同表上的索引,它們也要求其名稱的唯一性。這些瑣碎的規(guī)則都要納入我的索引改寫算法的考量之中。
3)Schema 名稱改寫
我對于 Schema(Schema 這個(gè)詞語的含義是 DBMS 系統(tǒng)中的數(shù)據(jù)庫實(shí)例,上文講的 ds0、ds1 就是兩個(gè)數(shù)據(jù)庫實(shí)例) 管理的方式與管理表的方式如出一轍,即采用邏輯 Schema 去管理一組數(shù)據(jù)源。因此,對于包含 Schema 的 SQL,我需要將用戶在 SQL 中書寫的邏輯 Schema 改寫為真實(shí)的數(shù)據(jù)庫分片 Schema。但我目前還不支持在 DQL(數(shù)據(jù)查詢語言,SELECT)和 DML(數(shù)據(jù)操縱語言,INSERT、UPDATE、DELETE 等)語句中使用 Schema,我只能改寫數(shù)據(jù)庫管理語句中的 Schema,例如:
SHOW?COLUMNS?FROM?t_order?FROM?order_ds;
我對這句數(shù)據(jù)庫管理語句的處理的方式是,將邏輯 Schema 改寫為隨機(jī)查找到的一個(gè)正確的真實(shí) Schema。這很簡單粗暴,但合理,因?yàn)槊總€(gè) Schema 中的 t_order 表的 COLUMNS 都是一樣的。
二、補(bǔ)列
1)排序補(bǔ)列
如下所示的一個(gè) SQL 語句,查詢邏輯表 t_order 中的 order_id 和 user_id,并且得到的結(jié)果根據(jù) user_id 降序排列,這個(gè)語句經(jīng)過路由和改寫之后在我的內(nèi)核的執(zhí)行階段執(zhí)行起來顯然沒有什么問題。
SELECT?order_id,?user_id?FROM?t_order?ORDER?BY?user_id;
但如果 SQL 語句是:
SELECT?order_id?FROM?t_order0?ORDER?BY?user_id;
我的內(nèi)核在執(zhí)行階段就無法執(zhí)行,因?yàn)檫@個(gè)語句查詢的結(jié)果只有 order_id,但卻要按照每個(gè) order_id 對應(yīng)的 user_id 排列 order_id,而結(jié)果集中沒有 user_id 列。所以,我的內(nèi)核在補(bǔ)列階段要對這個(gè) SQL 補(bǔ)充一列 user_id,補(bǔ)列的結(jié)果為:
SELECT?order_id,?user_id?FROM?t_order0?ORDER?BY?user_id;
再比如:
--?補(bǔ)列前(結(jié)果集?o.*?中不包含排序鍵?order_item_id)
SELECT?o.*?FROM?t_order?o,?t_order_item?i?WHERE?o.order_id=i.order_id?ORDER?BY?user_id,?order_item_id;
--?補(bǔ)列后(結(jié)果集?o.*?中包含排序鍵?order_item_id)
SELECT?o.*,?order_item_id?FROM?t_order?o,?t_order_item?i?WHERE?o.order_id=i.order_id?ORDER?BY?user_id,?order_item_id;
2)分組補(bǔ)列
和排序補(bǔ)列類似,分組補(bǔ)列的目的是在結(jié)果字段中補(bǔ)全分組鍵,比如:
--?補(bǔ)列前(結(jié)果集?order_id?中不包含分組鍵?user_id)
SELECT?order_id?FROM?t_order?GROUP?BY?user_id
--?補(bǔ)列后(結(jié)果集?order_id?中包含分組鍵?user_id)
SELECT?order_id,?user_id?FROM?t_order?GROUP?BY?user_id
3)聚合補(bǔ)列
分組和排序補(bǔ)列是簡單的補(bǔ)列處理情形。復(fù)雜的補(bǔ)列情形如處理使用 AVG 等聚合函數(shù)的 SQL 語句的補(bǔ)列。
將邏輯表 t_order 僅使用 order_id 為分片鍵水平分片成 3 個(gè)物理表 t_order0、t_order1、t_order2。使用 avg1 + avg2 + avg3 / 3 計(jì)算邏輯表的某列的平均值并不正確,正確的算法為 (sum1 + sum2 + sum3) / (count1 + count2 + count3)。這就需要將包含 AVG 的 SQL 改寫為 SUM 和 COUNT,并在結(jié)果歸并時(shí)重新計(jì)算平均值。例如以下 SQL:
SELECT?AVG(age)?FROM?t_user?WHERE?age>=18;
會(huì)被補(bǔ)列處理成:
SELECT?COUNT(age)?AS?AVG_DERIVED_COUNT,?SUM(age)?AS?AVG_DERIVED_SUM?FROM?t_user?WHERE?age>=18;
再經(jīng)過路由和改寫,最終執(zhí)行的 SQL 為:
SELECT?COUNT(age)?AS?AVG_DERIVED_COUNT,?SUM(age)?AS?AVG_DERIVED_SUM?FROM?t_user0?WHERE?age>=18;
SELECT?COUNT(age)?AS?AVG_DERIVED_COUNT,?SUM(age)?AS?AVG_DERIVED_SUM?FROM?t_user1?WHERE?age>=18;
SELECT?COUNT(age)?AS?AVG_DERIVED_COUNT,?SUM(age)?AS?AVG_DERIVED_SUM?FROM?t_user2?WHERE?age>=18;
最后,按照 (sum1 + sum2 + sum3) / (count1 + count2 + count3)在結(jié)果歸并時(shí)計(jì)算出正確的平均值。
這很好理解,打個(gè)比方,一個(gè)學(xué)校四年級學(xué)生全部有 400 人,被水平分片到 4 個(gè)班級,分別是四(1)班、四(2)班、四(3)班、四(4)班,各班人數(shù) 100 左右。一次期末考試之后,統(tǒng)計(jì)整個(gè)四年級的平均成績,一定是:
(
?四(1)班總分?+
?四(2)班總分?+
?四(3)班總分?+
?四(4)班總分
)?/?(
?四(1)班人數(shù)?+
?四(2)班人數(shù)?+
?四(3)班人數(shù)?+
?四(4)班人數(shù)
)
而不會(huì)是:
(
?四(1)班平均分?+
?四(2)班平均分?+
?四(3)班平均分?+
?四(4)班平均分
)?/?4
4)自增主鍵補(bǔ)列
還有一種補(bǔ)列發(fā)生在執(zhí)行 INSERT 的 SQL 語句時(shí)。
INSERT 語句如果使用數(shù)據(jù)庫自增主鍵,是無需寫入主鍵字段的,依靠數(shù)據(jù)庫實(shí)例本身自動(dòng)產(chǎn)生自增主鍵。但單個(gè)數(shù)據(jù)庫實(shí)例產(chǎn)生的自增主鍵是無法滿足數(shù)據(jù)表多分片場景下的主鍵的唯一性要求的,因此我提供了分布式自增主鍵的生成算法(如雪花算法),并且可以通過補(bǔ)列,讓使用方無需改動(dòng)現(xiàn)有代碼,即可將數(shù)據(jù)庫現(xiàn)有的自增主鍵透明地替換成分布式自增主鍵。舉例說明,假設(shè)表 t_order 的主鍵是 order_id,原始的 SQL 為:
INSERT?INTO?t_example?(`field1`,?`field2`)?VALUES?(10,?1);
可以看到,上述 SQL 中并未包含自增主鍵,是需要數(shù)據(jù)庫自行填充的,如果我不干預(yù),數(shù)據(jù)庫會(huì)使用一個(gè)局部自增主鍵來填充,這可能會(huì)造成全局范圍內(nèi)的多個(gè) t_order 分片表里包含重復(fù)主鍵。但有我在,我就不會(huì)讓數(shù)據(jù)庫使用它自己的局部自增主鍵,而是使用我提供的分布式自增主鍵。因此,SQL 將被改寫為:
INSERT?INTO?t_example?(id,?`field1`,?`field2`)?VALUES?(snow_flake_id,?10,?1);
上述 SQL 中的 snow_flake_id 表示自動(dòng)生成的分布式全局自增主鍵值。
顯然,所有的補(bǔ)列都是基于 SQL 語義進(jìn)行的,有賴于 SQL 的詞法和語法分析。因此,我還是要重復(fù)那句話:SQL 解析是 SQL 路由、改寫、執(zhí)行和歸并的基礎(chǔ)。
三、分頁修正
從多個(gè)表分片中獲取分頁數(shù)據(jù)與單表的場景是不同的。假設(shè)每 10 條數(shù)據(jù)為一頁,要從一個(gè)邏輯表中查詢 2 頁數(shù)據(jù)。在分片環(huán)境下從每個(gè)物理分片中獲取 LIMIT 10, 10,歸并之后再根據(jù)排序條件取出前 10 條數(shù)據(jù)是不正確的。
舉例說明,假設(shè) t_order 根據(jù) order_iid % 2 分成兩片,若對邏輯表 t_order 分頁查詢的 SQL 為:
SELECT?age?FROM?t_user?ORDER?BY?age?DESC?LIMIT?1,?2;
若直接路由并改寫成:
SELECT?age?FROM?t_user0?ORDER?BY?age?DESC?LIMIT?1,?2;
SELECT?age?FROM?t_user1?ORDER?BY?age?DESC?LIMIT?1,?2;
得到的結(jié)果會(huì)出乎你的預(yù)料,下圖展示了不進(jìn)行 SQL 的改寫的分頁執(zhí)行結(jié)果。

通過圖中所示,想要取得兩個(gè)分片表中共同的按照分?jǐn)?shù)排序的第 2 條和第 3 條數(shù)據(jù),應(yīng)該是 95 和 90。由于執(zhí)行的 SQL 只能從每個(gè)表中獲取第 2 條和第 3 條數(shù)據(jù),即從 t_user0 表中獲取的是 90 和 80;從 t_user1 表中獲取的是 85 和 75。因此進(jìn)行結(jié)果歸并時(shí),只能從獲取的 90,80,85 和 75 之中進(jìn)行歸并,那么結(jié)果歸并無論怎么實(shí)現(xiàn),都不可能獲得正確的結(jié)果。
正確的做法是將分頁條件改寫為 LIMIT 0, 3,取出所有前兩頁數(shù)據(jù),再結(jié)合排序條件計(jì)算出正確的數(shù)據(jù)。即:
SELECT?age?FROM?t_user?ORDER?BY?age?DESC?LIMIT?0,?3;
路由并改寫之后的結(jié)果為:
SELECT?age?FROM?t_user0?ORDER?BY?age?DESC?LIMIT?0,?3;
SELECT?age?FROM?t_user1?ORDER?BY?age?DESC?LIMIT?0,?3;
下圖展示了進(jìn)行正確的 SQL 改寫之后的分頁執(zhí)行結(jié)果:

在這種做法下,獲取數(shù)據(jù)的偏移量位置越靠后,使用 LIMIT 分頁方式的效率就越低。但有很多方法可以避免使用 LIMIT 進(jìn)行分頁。比如使用上次分頁數(shù)據(jù)結(jié)尾 ID 作為下次查詢條件的分頁方式等(我會(huì)在后文給出示例)。
四、批量拆分
1)批量插入拆分
在處理批量插入的 SQL 時(shí),如果插入的數(shù)據(jù)是跨分片的,那么需要對 SQL 進(jìn)行改寫來防止將多余的數(shù)據(jù)寫入到數(shù)據(jù)庫中。舉例說明,如下 SQL:
INSERT?INTO?t_order?(order_id,?xxx)?VALUES?(1,?'xxx'),?(2,?'xxx'),?(3,?'xxx');
假設(shè)數(shù)據(jù)表 t_order 仍然是按照 order_id 的奇偶值分為兩片的,僅將這條 SQL 中的表名進(jìn)行修改,然后發(fā)送至數(shù)據(jù)庫完成 SQL 的執(zhí)行,則兩個(gè)分片都會(huì)寫入相同的記錄。雖然只有符合分片查詢條件的數(shù)據(jù)才能夠被查詢語句取出,但存在冗余數(shù)據(jù)的實(shí)現(xiàn)方案并不合理。因此我需要將路由后的 SQL 改寫為:
INSERT?INTO?t_order0?(order_id,?xxx)?VALUES?(2,?'xxx');
INSERT?INTO?t_order1?(order_id,?xxx)?VALUES?(1,?'xxx'),?(3,?'xxx');
2)In 查詢拆分
使用 IN 的批量查詢與批量插入的情況相似,不過使用 IN 的批量查詢操作并不會(huì)導(dǎo)致數(shù)據(jù)查詢結(jié)果錯(cuò)誤(批量插入操作與批量查詢操作的不同之處在于,查詢語句中即使用了不存在于當(dāng)前分片的分片鍵值,也不會(huì)對結(jié)果產(chǎn)生影響。因此對批量查詢 SQL 進(jìn)行拆分并不是必須的,而插入操作則必須將多余的分片鍵值刪除)。
因此對于如以下 SQL 的批量拆分改寫,我偷了個(gè)懶:
SELECT?*?FROM?t_order?WHERE?order_id?IN?(1,?2,?3);
直接路由并改寫為:
SELECT?*?FROM?t_order0?WHERE?order_id?IN?(1,?2,?3);
SELECT?*?FROM?t_order1?WHERE?order_id?IN?(1,?2,?3);
實(shí)際上,更好的改寫結(jié)果是:
SELECT?*?FROM?t_order0?WHERE?order_id?IN?(2);
SELECT?*?FROM?t_order1?WHERE?order_id?IN?(1,?3);
這樣可以進(jìn)一步的提升查詢性能,但我的創(chuàng)造者給我設(shè)計(jì)的內(nèi)核并沒有進(jìn)行這種優(yōu)化。雖然 SQL 的執(zhí)行結(jié)果是正確的,但并未達(dá)到最優(yōu)的查詢效率。
4.3.4. SQL 執(zhí)行
在完成 SQL 解析、改寫和路由之后,我終于要執(zhí)行 SQL 了!但這也是我的內(nèi)核最復(fù)雜的工作部分。
我擁有一個(gè)自動(dòng)化的 SQL 執(zhí)行引擎,它負(fù)責(zé)將改寫和路由完成之后的真實(shí) SQL 安全且高效發(fā)送到底層數(shù)據(jù)源執(zhí)行。它不是簡單地將 SQL 通過 JDBC 直接發(fā)送至數(shù)據(jù)源執(zhí)行,也并非直接將執(zhí)行請求放入線程池去并發(fā)執(zhí)行,而是采用了復(fù)雜的控制策略。我的執(zhí)行引擎的工作目標(biāo)是平衡資源占用(資源包括數(shù)據(jù)庫連接、內(nèi)存和線程)與執(zhí)行效率(時(shí)間)。
在講解我的執(zhí)行引擎執(zhí)行 SQL 的過程之前,我要先向各位介紹我的執(zhí)行引擎的連接模式。
一個(gè)面向邏輯表編寫的 SQL 交到我的手中,會(huì)被我路由、改寫成面向多個(gè)物理分片表的 SQL(也可以稱為真實(shí) SQL)。執(zhí)行多個(gè)真實(shí) SQL,最理想的情況是為每個(gè)分片 SQL 查詢創(chuàng)建一個(gè)數(shù)據(jù)庫連接,且每個(gè)連接交由一個(gè)專門的線程來處理。但是計(jì)算機(jī)系統(tǒng)所能提供的資源是有限的,不可能讓進(jìn)程無限創(chuàng)建數(shù)據(jù)庫連接和線程。
從資源控制的角度看,業(yè)務(wù)方訪問數(shù)據(jù)庫的連接數(shù)量應(yīng)當(dāng)有所限制(你們常用的數(shù)據(jù)庫連接池就在做這件事)。它能夠有效地防止某一業(yè)務(wù)操作過多地占用資源,從而將數(shù)據(jù)庫連接的資源耗盡,以致于影響其他業(yè)務(wù)的正常訪問。特別是,在一個(gè)數(shù)據(jù)庫實(shí)例中存在較多分片表的情況下,一條不包含分片鍵的邏輯 SQL 經(jīng)過路由過程將產(chǎn)生大量落在同庫不同分片表的真實(shí) SQL,如果每條真實(shí) SQL 都占用一個(gè)獨(dú)立的連接,那么一次查詢無疑將會(huì)占用過多的資源。
從執(zhí)行效率的角度看,為每個(gè)分片查詢維持一個(gè)獨(dú)立的數(shù)據(jù)庫連接,可以更加有效的利用多線程來提升執(zhí)行效率,因?yàn)槿魹槊總€(gè)數(shù)據(jù)庫連接開啟獨(dú)立的處理線程,可以并行處理查詢結(jié)果集。而且,為每個(gè)分片查詢維持一個(gè)獨(dú)立的數(shù)據(jù)庫連接,還能夠避免過早的將查詢結(jié)果集加載至數(shù)據(jù)庫客戶端(我,Sharding-JDBC,數(shù)據(jù)庫中間件,運(yùn)行在應(yīng)用程序所在的 JVM 上,就是一個(gè)數(shù)據(jù)庫客戶端)的內(nèi)存,代以流式處理方式來處理。若為每個(gè)分片查詢維持一個(gè)獨(dú)立的數(shù)據(jù)庫連接,能夠持有查詢結(jié)果集游標(biāo)位置的引用,在需要獲取相應(yīng)數(shù)據(jù)時(shí)移動(dòng)游標(biāo)即可。以結(jié)果集游標(biāo)下移進(jìn)行結(jié)果歸并的方式,稱之為流式歸并,它無需將結(jié)果數(shù)據(jù)全數(shù)加載至數(shù)據(jù)庫客戶端內(nèi)存,可以有效的節(jié)省數(shù)據(jù)庫客戶端內(nèi)存資源,進(jìn)而減少數(shù)據(jù)庫客戶端垃圾回收的頻次(說的簡單些,即先將查詢結(jié)果集保留在數(shù)據(jù)庫服務(wù)器的緩沖區(qū)內(nèi),然后客戶端這邊采用流式處理方式一點(diǎn)點(diǎn)獲取數(shù)據(jù)來處理。避免一次性將結(jié)果集送到客戶端,占用客戶端太多內(nèi)存)。當(dāng)無法保證每個(gè)分片查詢持有一個(gè)獨(dú)立數(shù)據(jù)庫連接時(shí),則需要在復(fù)用該數(shù)據(jù)庫連接獲取下一個(gè)分片查詢的結(jié)果集之前,將當(dāng)前的分片查詢結(jié)果集全數(shù)加載至內(nèi)存。因此,即使可以采用流式歸并,在此場景下也將退化為內(nèi)存歸并。
綜上所述,我的執(zhí)行引擎一方面想控制數(shù)據(jù)庫連接的數(shù)量;另一方面想為每個(gè)分片查詢維持一個(gè)獨(dú)立的數(shù)據(jù)庫連接,以采用更優(yōu)的流式歸并模式達(dá)到對數(shù)據(jù)庫客戶端內(nèi)存資源的節(jié)省。如何處理好兩者之間的關(guān)系,是我的執(zhí)行引擎需要解決的問題。
舉個(gè)例子,如果一條邏輯 SQL 在經(jīng)過我的路由和改寫處理之后,需要操作某數(shù)據(jù)庫實(shí)例下的 200 張分表。那么,是選擇創(chuàng)建 200 個(gè)連接并行執(zhí)行,還是選擇創(chuàng)建一個(gè)連接串行執(zhí)行呢?效率與資源控制又應(yīng)該如何抉擇呢?針對上述場景,我的執(zhí)行引擎提供了一種解決思路。它提出了連接模式(Connection Mode)的概念,將其劃分為內(nèi)存限制模式(MEMORY_STRICTLY)和連接限制模式(CONNECTION_STRICTLY)這兩種類型。內(nèi)存限制模式要求更多的連接,但占用更少的客戶端內(nèi)存;而連接限制模式要求更少的連接,但占用更多的客戶端內(nèi)存。
一、內(nèi)存限制模式
在這種模式下,我的執(zhí)行引擎對一次操作所耗費(fèi)的數(shù)據(jù)庫連接數(shù)量不做限制。如果實(shí)際執(zhí)行的 SQL 需要對某數(shù)據(jù)庫實(shí)例中的 200 張分片表做操作,則對每張分片表創(chuàng)建一個(gè)新的數(shù)據(jù)庫連接,并通過多線程的方式并發(fā)處理,以達(dá)成執(zhí)行效率最大化。并且在 SQL 滿足條件情況下,優(yōu)先選擇流式歸并,以防止數(shù)據(jù)庫客戶端出現(xiàn)內(nèi)存溢出或避免頻繁垃圾回收情況。
二、連接限制模式
在這種模式下,我的執(zhí)行引擎嚴(yán)格控制對一次操作所耗費(fèi)的數(shù)據(jù)庫連接數(shù)量。如果實(shí)際執(zhí)行的 SQL 需要對某數(shù)據(jù)庫實(shí)例中的 200 張分片表做操作,那么只會(huì)創(chuàng)建唯一的數(shù)據(jù)庫連接,并對其 200 張分片表串行處理。如果一次操作中的分片散落在不同的數(shù)據(jù)庫,仍然采用多線程處理對不同庫的操作,但每個(gè)庫的每次操作仍然只創(chuàng)建一個(gè)唯一的數(shù)據(jù)庫連接。這樣即可以防止對一次請求對數(shù)據(jù)庫連接占用過多所帶來的問題。該模式始終選擇內(nèi)存歸并。
內(nèi)存限制模式適用于 OLAP(以讀操作為主)操作,可以通過放寬對數(shù)據(jù)庫連接的限制提升系統(tǒng)吞吐量;連接限制模式適用于 OLTP (以寫操作為主)操作。OLTP 通常帶有分片鍵,會(huì)路由到單一的分片,因此嚴(yán)格控制數(shù)據(jù)庫連接,以保證在線系統(tǒng)數(shù)據(jù)庫資源能夠被更多的應(yīng)用所使用。
我最初想將使用何種模式的決定權(quán)交由你們這些工程師來配置,讓你們依據(jù)自己業(yè)務(wù)的實(shí)際場景需求選擇使用內(nèi)存限制模式或連接限制模式。這種想法將兩難的選擇的決定權(quán)交由用戶,使得用戶必須要了解這兩種模式的利弊,并依據(jù)業(yè)務(wù)場景需求進(jìn)行選擇。而且這種靜態(tài)的連接模式配置,缺乏靈活性。
在實(shí)際的使用場景中,面對不同的邏輯 SQL,每次的路由結(jié)果是不同的。這就意味著某些操作可能需要使用內(nèi)存歸并,而某些操作則可能選擇流式歸并更優(yōu),具體采用哪種方式不應(yīng)該由用戶在我啟動(dòng)之前配置好,而是應(yīng)該根據(jù)具體的邏輯 SQL,來動(dòng)態(tài)地決定連接模式。
為了降低用戶的使用成本,以及讓連接模式能夠動(dòng)態(tài)變化,我的執(zhí)行引擎在其內(nèi)部消化了連接模式概念(可我還是認(rèn)為應(yīng)該告訴你們這些被屏蔽的東西,畢竟技術(shù)的原理和優(yōu)秀的設(shè)計(jì)思想是促進(jìn)你們進(jìn)步的重要因素),根據(jù)當(dāng)前場景自動(dòng)選擇最優(yōu)的執(zhí)行方案。
我的執(zhí)行引擎將連接模式的選擇粒度細(xì)化至每一次邏輯 SQL 請求。針對每次邏輯 SQL 請求,我的執(zhí)行引擎都將根據(jù)其路由結(jié)果,進(jìn)行實(shí)時(shí)的演算和權(quán)衡,并自主地采用恰當(dāng)?shù)倪B接模式執(zhí)行,以達(dá)到資源控制和效率的最優(yōu)平衡。
針對這種自動(dòng)化的執(zhí)行引擎,用戶只需配置 maxConnectionSizePerQuery 即可,該參數(shù)表示進(jìn)行一次邏輯查詢時(shí)每個(gè)數(shù)據(jù)庫所允許使用的最大連接數(shù),這是我的執(zhí)行引擎進(jìn)行演算和權(quán)衡的重要參數(shù)。
好了,我的執(zhí)行引擎提供的連接模式講完了,我可以給你們講我的執(zhí)行引擎執(zhí)行 SQL 的過程了。我的執(zhí)行引擎把執(zhí)行 SQL 分為準(zhǔn)備和執(zhí)行兩個(gè)階段。
一、準(zhǔn)備階段
準(zhǔn)備階段分為結(jié)果集分組和執(zhí)行單元?jiǎng)?chuàng)建兩個(gè)步驟。
結(jié)果集分組是實(shí)現(xiàn)內(nèi)化連接模式(向使用我的工程師屏蔽內(nèi)存限制模式或連接限制模式的選擇)概念的關(guān)鍵,結(jié)果集分組的工作,一言以蔽之,就是決定每個(gè)連接要處理的查詢請求/要執(zhí)行的 SQL。結(jié)果集分組具體步驟如下:
1)先將 SQL 的路由結(jié)果按照數(shù)據(jù)源的名稱進(jìn)行分組;
2)然后通過下圖的公式,可以獲得每個(gè)數(shù)據(jù)庫實(shí)例在 maxConnectionSizePerQuery 的允許范圍內(nèi),每個(gè)連接需要執(zhí)行的 SQL 路由結(jié)果組,并計(jì)算出本次請求的最優(yōu)連接模式。

在 maxConnectionSizePerQuery 允許的范圍內(nèi),當(dāng)一個(gè)連接需要執(zhí)行的請求數(shù)量大于 1 時(shí),意味著當(dāng)前的數(shù)據(jù)庫連接無法持有相應(yīng)的分片結(jié)果集,則必須采用內(nèi)存歸并;反之,當(dāng)一個(gè)連接需要執(zhí)行的請求數(shù)量等于 1 時(shí),意味著當(dāng)前的數(shù)據(jù)庫連接可以持有相應(yīng)的分片結(jié)果集,則可以采用流式歸并。每一次的連接模式的選擇,是針對每一個(gè)物理數(shù)據(jù)庫的。也就是說,在同一次查詢中,如果該查詢被路由至一個(gè)以上的數(shù)據(jù)庫,每個(gè)數(shù)據(jù)庫的連接模式不一定一樣,它們可能是混合存在的形態(tài)。
通過上一步驟獲得的路由分組結(jié)果創(chuàng)建執(zhí)行的單元,執(zhí)行單元包括連接+該連接上要執(zhí)行的 SQL。當(dāng)數(shù)據(jù)源使用數(shù)據(jù)庫連接池等控制數(shù)據(jù)庫連接數(shù)量的技術(shù)時(shí),在獲取數(shù)據(jù)庫連接時(shí),如果不妥善處理并發(fā),則有一定幾率發(fā)生死鎖。在多個(gè)請求相互等待對方釋放數(shù)據(jù)庫連接資源時(shí),將會(huì)產(chǎn)生饑餓等待,造成交叉的死鎖問題。舉例說明,假設(shè)一次查詢需要在某一數(shù)據(jù)源上獲取兩個(gè)數(shù)據(jù)庫連接,并路由至同一個(gè)數(shù)據(jù)庫的兩個(gè)分表查詢。則有可能出現(xiàn)查詢 A 已獲取到該數(shù)據(jù)源的 1 個(gè)數(shù)據(jù)庫連接,并等待獲取另一個(gè)數(shù)據(jù)庫連接;而查詢 B 也已經(jīng)在該數(shù)據(jù)源上獲取到的一個(gè)數(shù)據(jù)庫連接,并同樣等待另一個(gè)數(shù)據(jù)庫連接的獲取。如果數(shù)據(jù)庫連接池的允許最大連接數(shù)是 2,那么這 2 個(gè)查詢請求將永久的等待下去。下圖描繪了死鎖的情況。

我為了避免死鎖的出現(xiàn),在獲取數(shù)據(jù)庫連接時(shí)進(jìn)行了同步處理。具體來說就是在創(chuàng)建執(zhí)行單元時(shí),以原子性的方式一次性獲取本次 SQL 請求所需的全部數(shù)據(jù)庫連接,杜絕了每次查詢請求獲取到部分資源的可能。由于這樣做會(huì)導(dǎo)致每次獲取數(shù)據(jù)庫連接時(shí)都進(jìn)行連接鎖定,這會(huì)降低我執(zhí)行 SQL 的并發(fā)度。因此,我在這?進(jìn)行了 2 點(diǎn)優(yōu)化:
1)避免鎖定一次性只需要獲取 1 個(gè)數(shù)據(jù)庫連接的操作。因?yàn)槊看蝺H需要獲取 1 個(gè)連接,則不會(huì)發(fā)生兩個(gè)請求相互等待的場景,無需鎖定。對于大部分 OLTP 的操作,都是使用分片鍵路由至唯一的數(shù)據(jù)節(jié)點(diǎn),這會(huì)使得系統(tǒng)變?yōu)橥耆珶o鎖的狀態(tài),進(jìn)一步提升了并發(fā)效率。
2)僅針對內(nèi)存限制模式時(shí)才進(jìn)行資源鎖定。在使用連接限制模式時(shí),所有的查詢結(jié)果集將在裝載至內(nèi)存之后釋放掉數(shù)據(jù)庫連接資源,因此不會(huì)產(chǎn)生死鎖等待的問題。
二、執(zhí)行階段
該階段用于真正地執(zhí)行 SQL,它分為分組執(zhí)行和查詢結(jié)果集生成兩個(gè)步驟。
1)分組執(zhí)行:分組執(zhí)行將準(zhǔn)備執(zhí)行階段生成的執(zhí)行單元分組下發(fā)至我的底層執(zhí)行引擎,并針對執(zhí)行過程中的每個(gè)關(guān)鍵步驟發(fā)送事件。如:執(zhí)行開始事件、執(zhí)行成功事件以及執(zhí)行失敗事件。我的執(zhí)行引擎僅關(guān)注事件的發(fā)送,它并不關(guān)心事件的訂閱者。我的其他模塊,如:分布式事務(wù)、調(diào)用鏈路追蹤等,會(huì)訂閱感興趣的事件,并進(jìn)行相應(yīng)的處理。我通過在執(zhí)行準(zhǔn)備階段的獲取的連接模式,生成內(nèi)存查詢結(jié)果集或流式查詢結(jié)果集,并將其傳遞至結(jié)果歸并引擎,以進(jìn)行下一步的?作。
我的執(zhí)行引擎的整體工作流如下圖所示。

2)歸并結(jié)果集:請看下一節(jié)。
4.3.5. 結(jié)果歸并
我建議你好好看看這一節(jié),它里面包含了很多數(shù)據(jù)結(jié)構(gòu)的知識(shí)。
我將從各個(gè)數(shù)據(jù)分片上獲取的結(jié)果集,組合成為一個(gè)總的結(jié)果集并正確的返回至請求客戶端,這個(gè)過程就是結(jié)果歸并。
我支持的結(jié)果歸并從結(jié)構(gòu)上劃分,可分為流式歸并、內(nèi)存歸并和裝飾者歸并:
1)流式歸并
流式歸并是指在實(shí)施歸并的時(shí)候,不需要將所有分片上的查詢結(jié)果全部都加載進(jìn)客戶端內(nèi)存,只需要把每個(gè)分片的查詢結(jié)果一點(diǎn)點(diǎn)地取到內(nèi)存里面進(jìn)行歸并處理,最終能夠逐條產(chǎn)生歸并的結(jié)果。后文要講的遍歷歸并、排序歸并以及流式分組歸并都屬于流式歸并。
2)內(nèi)存歸并
內(nèi)存歸并則是指需要將所有的分片結(jié)果集加載到內(nèi)存中,再通過統(tǒng)一的分組、排序以及聚合等計(jì)算之后,再將其封裝成為能被請求客戶端逐條訪問的歸并結(jié)果集返回。
3)裝飾者歸并
裝飾者歸并是指對常規(guī)的結(jié)果集歸并利用裝飾者模式進(jìn)行功能增強(qiáng),目前裝飾者歸并有分頁裝飾歸并和聚合裝飾歸并這 2 種類型。我在前文講過,包含聚合函數(shù)的 SQL 經(jīng)過改寫之后要在歸并階段重新計(jì)算聚合,這就是裝飾者歸并要做的事情;同樣,包含分頁信息的 SQL 經(jīng)過改寫之后要在歸并階段重新進(jìn)行分頁計(jì)算,這也是裝飾者歸并要做的事情。
我支持的結(jié)果歸并從功能上分為遍歷、排序、分組、分頁和聚合 5 種類型:
1)遍歷歸并
它是最為簡單的歸并方式。只需將多個(gè)分片結(jié)果集合并為一個(gè)單向鏈表即可。在遍歷完成鏈表中當(dāng)前分片結(jié)果集之后,將鏈表元素后移一位,繼續(xù)遍歷下一個(gè)分片結(jié)果集即可。
例如,邏輯表 t_user 在單個(gè)數(shù)據(jù)源(不做分庫)中根據(jù) user_id % 3 的結(jié)果分成三片 t_user0、t_user1 和 t_user2,當(dāng)查詢的邏輯 SQL 為:
SELECT?age?FROM?t_user?where?age?18
它被路由和改寫之后的結(jié)果為:
SELECT?age?FROM?t_user0?where?age?18
SELECT?age?FROM?t_user1?where?age?18
SELECT?age?FROM?t_user2?where?age?18
顯然它最終產(chǎn)生三個(gè)分片結(jié)果集,對這三個(gè)結(jié)果集進(jìn)行歸并,只需將他們串聯(lián)成鏈表返回給請求客戶端即可。請求客戶端讀取總的歸并結(jié)果集,也就是按照鏈表元素次序,一個(gè)分片結(jié)果集讀完后,再到下一個(gè)分片結(jié)果集去讀取。顯然這個(gè)過程是可以使用流式處理方式的,即不需要事先把三個(gè)分片結(jié)果集一次性全部加載到內(nèi)存。
2)排序歸并
例如,邏輯表 t_user 在單個(gè)數(shù)據(jù)源(不做分庫)中根據(jù) user_id % 3 的結(jié)果分成三片 t_user0、t_user1 和 t_user2,當(dāng)查詢的邏輯 SQL 為:
SELECT?age?FROM?t_user?order?by?age?DESC
它被路由和改寫之后的結(jié)果為:
SELECT?age?FROM?t_user0?order?by?age?DESC
SELECT?age?FROM?t_user1?order?by?age?DESC
SELECT?age?FROM?t_user2?order?by?age?DESC
由于在 SQL 中存在 ORDER BY 語句,因此每個(gè)分片結(jié)果集自身是有序的,因此只需要將分片結(jié)果集當(dāng)前游標(biāo)指向的數(shù)據(jù)值進(jìn)行排序即可。這相當(dāng)于對多個(gè)有序的數(shù)組進(jìn)行排序,歸并排序是最適合此場景的排序算法。
我在對帶 ORDER BY 語句的分片查詢結(jié)果進(jìn)行歸并時(shí),會(huì)將每個(gè)結(jié)果集的當(dāng)前數(shù)據(jù)值進(jìn)行比較,并將其放入優(yōu)先級隊(duì)列。每次獲取下一條數(shù)據(jù)時(shí),只需將隊(duì)列頂端結(jié)果集的游標(biāo)下移,并根據(jù)新游標(biāo)重新進(jìn)入優(yōu)先級排序隊(duì)列找到自己的位置即可。
下圖展示了 3 張分片表返回的分片結(jié)果集,每個(gè)分片結(jié)果集已經(jīng)根據(jù)分?jǐn)?shù)排序完畢,但是 3 個(gè)分片結(jié)果集之間是無序的。將 3 個(gè)分片結(jié)果集的當(dāng)前游標(biāo)指向的數(shù)據(jù)值進(jìn)行排序,并放入優(yōu)先級隊(duì)列,t_user0 的第一個(gè)數(shù)據(jù)值最大,t_user2 的第一個(gè)數(shù)據(jù)值次之,t_user1 的第一個(gè)數(shù)據(jù)值最小,因此優(yōu)先級隊(duì)列根據(jù) t_user0、t_user2 和 t_user1 的方式排序隊(duì)列。

下圖則展現(xiàn)了進(jìn)行 next 調(diào)用的時(shí)候,排序歸并是如何進(jìn)行的。通過下圖你們可以看到,當(dāng)進(jìn)行第一次 next 調(diào)用時(shí),排在隊(duì)列首位的 t_user0 將會(huì)被彈出隊(duì)列,并且將當(dāng)前游標(biāo)指向的數(shù)據(jù)值(也就是 100)返回至查詢客戶端,并且將游標(biāo)下移一位之后,重新放入優(yōu)先級隊(duì)列。而優(yōu)先級隊(duì)列也會(huì)根據(jù) t_user0 的當(dāng)前數(shù)據(jù)結(jié)果集指向游標(biāo)的數(shù)據(jù)值(這?是 90)進(jìn)行排序,根據(jù)當(dāng)前數(shù)值,t_user0 排列在隊(duì)列的最后一位。之前隊(duì)列中排名第二的 t_user2 的分片結(jié)果集則自動(dòng)排在了隊(duì)列首位。

在進(jìn)行第二次 next 時(shí),只需要將目前排列在隊(duì)列首位的 t_user2 彈出隊(duì)列,并且將其數(shù)據(jù)結(jié)果集游標(biāo)指向的值返回至客戶端,并下移游標(biāo),繼續(xù)加入隊(duì)列排隊(duì),以此類推。當(dāng)一個(gè)結(jié)果集中已經(jīng)沒有數(shù)據(jù)了,則無需再次加入隊(duì)列。
可以看到,對于每個(gè)數(shù)據(jù)結(jié)果集中的數(shù)據(jù)有序,而多數(shù)據(jù)結(jié)果集整體無序的情況下,我無需將所有的數(shù)據(jù)都加載至內(nèi)存即可排序,我使用的是流式歸并的方式,每次 next 僅獲取唯一正確的一條數(shù)據(jù),極大的節(jié)省了內(nèi)存的消耗。
3)分組歸并
分組歸并的情況最為復(fù)雜,它分為流式分組歸并和內(nèi)存分組歸并。流式分組歸并要求 SQL 的排序項(xiàng)與分組項(xiàng)的字段必須保持一致,否則只能通過內(nèi)存歸并才能保證其數(shù)據(jù)的正確性。
舉例說明,假設(shè)邏輯表 t_socre(表結(jié)構(gòu)中包含考生的姓名 name、科目 subject 和分?jǐn)?shù) score,且為了簡單起見,不考慮重名的情況)根據(jù)科目分成 3 片:t_socre_java、t_socre_go、t_socre_python(后文插圖中的分片表均未展示科目字段,只展示姓名和分?jǐn)?shù)字段)?,F(xiàn)在要通過 SQL 獲取每位考生的總分:
SELECT?name,?SUM(score)?as?sum_score?FROM?t_score?GROUP?BY?name?ORDER?BY?name?asc;
以上 SQL 被路由和改寫之后的結(jié)果為:
SELECT?name,?SUM(score)?as?sum_score?FROM?t_score_java?GROUP?BY?name?ORDER?BY?name?asc;
SELECT?name,?SUM(score)?as?sum_score?FROM?t_score_go?GROUP?BY?name?ORDER?BY?name?asc;
SELECT?name,?SUM(score)?as?sum_score?FROM?t_score_python?GROUP?BY?name?ORDER?BY?name?asc;
在分組項(xiàng)與排序項(xiàng)完全一致的情況下,在三個(gè)分片表中取得的數(shù)據(jù)都是按照 name 字段升序排列的,每個(gè)分組所需的數(shù)據(jù)全部存在于各個(gè)分片結(jié)果集的當(dāng)前游標(biāo)所指向的數(shù)據(jù)值中,即每個(gè) name 的分?jǐn)?shù)全部存在于各個(gè)分片結(jié)果集的當(dāng)前游標(biāo)所指向的數(shù)據(jù)值中,因此可以采用流式歸并。如下圖所示:

進(jìn)行歸并時(shí),過程與排序歸并類似。下圖展現(xiàn)了進(jìn)行 next 調(diào)用的時(shí)候,流式分組歸并是如何進(jìn)行的。

通過上一張圖你們可以看到,當(dāng)進(jìn)行第一次 next 調(diào)用時(shí),按照當(dāng)前游標(biāo)所指記錄的 name 升序排列,排在隊(duì)列首位的 t_score_java 分片將會(huì)被彈出隊(duì)列,并且將 name 同為“Jetty”的其他分片結(jié)果集中的數(shù)據(jù)一同彈出隊(duì)列。在獲取了所有的 name 為“Jetty”的同學(xué)的分?jǐn)?shù)之后,進(jìn)行累加操作,得到“Jetty”的總分。與此同時(shí),所有的分片結(jié)果集中的游標(biāo)都將下移至數(shù)據(jù)值“Jetty”的下一個(gè)不同的數(shù)據(jù)值,并且根據(jù)分片結(jié)果集的當(dāng)前游標(biāo)所指記錄的 name 值進(jìn)行重排序。因此,包含名字“John”的相關(guān)數(shù)據(jù)結(jié)果集則排在的隊(duì)列的前列。
對于分組項(xiàng)與排序項(xiàng)不一致的情況,由于在每個(gè)分片結(jié)果集中分組字段的值并非有序的,因此無法使用流式歸并,需要將所有的分片結(jié)果集數(shù)據(jù)加載至內(nèi)存中進(jìn)行分組和聚合。例如,若通過以下 SQL 獲取每位考生的總分并按照分?jǐn)?shù)從高至低排序:
SELECT?name,?SUM(score)?as?sum_score?FROM?t_score?GROUP?BY?name?ORDER?BY?score?DESC;
那么各個(gè)分片結(jié)果集中的數(shù)據(jù)如下圖所示,顯然是無法像上圖那樣進(jìn)行流式歸并的,不信你按照上一張圖的過程動(dòng)筆畫一下試試 :-)

當(dāng) SQL 中只包含分組語句時(shí),我會(huì)通過 SQL 改寫,自動(dòng)給 SQL 增加與分組項(xiàng)一致的排序項(xiàng),這一點(diǎn)我在講述 SQL 改寫的沒有說,我放在這里說你會(huì)更加明白我的意圖:這能夠使得這句 SQL 的歸并階段從消耗內(nèi)存的內(nèi)存分組歸并方式轉(zhuǎn)化為流式分組歸并方式。
4)聚合歸并
聚合函數(shù)可以分為比較、累加和求平均值這 3 種類型。
比較類型的聚合函數(shù)是指 MAX 和 MIN。它們需要對每一個(gè)同組的結(jié)果集數(shù)據(jù)進(jìn)行比較,并且直接返回其最大或最小值即可。
舉例說明,假設(shè)邏輯表 t_socre(表結(jié)構(gòu)中包含考生的姓名 name、科目 subject 和分?jǐn)?shù) score,且為了簡單起見,不考慮重名的情況)根據(jù)科目分成 3 片:t_socre_java、t_socre_go、t_socre_python(后文插圖中的分片表均未展示科目字段,只展示姓名和分?jǐn)?shù)字段)?,F(xiàn)在要通過 SQL 獲取每位考生的單科最高分:
SELECT?name,?MAX(score)?FROM?t_score?GROUP?BY?name;
以上 SQL 被路由和改寫之后的結(jié)果為:
--當(dāng)?SQL?中只包含分組語句時(shí),我會(huì)通過?SQL?改寫,自動(dòng)增加與分組項(xiàng)一致的排序項(xiàng),這能夠使得這句?SQL?的歸并階段從消耗內(nèi)存的內(nèi)存分組歸并方式轉(zhuǎn)化為流式分組歸并方式
SELECT?name,?MAX(score)?as?max_score?FROM?t_score_java?GROUP?BY?name?ORDER?BY?name?ASC;
SELECT?name,?MAX(score)?as?max_score?FROM?t_score_go?GROUP?BY?name?ORDER?BY?name?ASC;
SELECT?name,?MAX(score)?as?max_score?FROM?t_score_python?GROUP?BY?name?ORDER?BY?name?ASC;

通過下一張圖你們可以看到,當(dāng)進(jìn)行第一次 next 調(diào)用時(shí),按照當(dāng)前游標(biāo)所指記錄的 name 升序排列,排在隊(duì)列首位的 t_score_java 分片將會(huì)被彈出隊(duì)列,并且將 name 同為“Jetty”的其他分片結(jié)果集中的數(shù)據(jù)一同彈出隊(duì)列。在獲取了所有的 name 為“Jetty”的同學(xué)的分?jǐn)?shù)之后,找出最大值,得到“Jetty”的單科最高分。與此同時(shí),所有的分片結(jié)果集中的游標(biāo)都將下移至數(shù)據(jù)值“Jetty”的下一個(gè)不同的數(shù)據(jù)值,并且根據(jù)分片結(jié)果集的當(dāng)前游標(biāo)所指記錄的 name 值進(jìn)行重排序。因此,包含名字“John”的相關(guān)數(shù)據(jù)結(jié)果集則排在的隊(duì)列的前列。
顯然,這一過程屬于流式歸并。

以上是 MAX 函數(shù)的聚合方式,MIN 函數(shù)的聚合方式類似,不再贅述。
累加類型的聚合函數(shù)是指 SUM 和 COUNT。它們需要將每一個(gè)同組的結(jié)果集數(shù)據(jù)進(jìn)行累加,在前面那個(gè)“獲取每位考生的總分并按照分?jǐn)?shù)從高至低排序”的實(shí)例中你們已經(jīng)見識(shí)過了,不再贅述。這一過程可以流式歸并方式。
求平均值的聚合函數(shù)只有 AVG。這必須通過 SQL 改寫出的 SUM 和 COUNT 進(jìn)行計(jì)算,相關(guān)內(nèi)容已在 SQL 改寫的內(nèi)容中涵蓋,不再贅述。這一過程可以流式歸并方式。
無論是流式分組歸并還是內(nèi)存分組歸并,對聚合函數(shù)的處理都是一致的,因此,聚合歸并是在之前介紹的歸并過程之上追加的歸并能力,即裝飾。實(shí)際上我的創(chuàng)造者正是通過裝飾者模式賦予我聚合歸并能力的。
5)分頁歸并
上文所述的所有歸并類型都可能進(jìn)行分頁。分頁也是追加在其他歸并類型之上的裝飾過程,我的創(chuàng)造者通過裝飾者模式賦予我對數(shù)據(jù)結(jié)果集的分頁能力。若邏輯 SQL 要查詢第 M 頁的數(shù)據(jù),查詢結(jié)果集會(huì)包含 N(N=路由后的 SQL 數(shù)量)個(gè)頁的數(shù)據(jù),分頁歸會(huì)將無需獲取的數(shù)據(jù)過濾掉,最終得到邏輯表的第 M 頁的數(shù)據(jù)。
在分片場景中,將 LIMIT 10000000, 10 改寫為 LIMIT 0, 10000010,才能保證其數(shù)據(jù)的正確性,這一點(diǎn)我在 SQL 改寫的分頁修正部分講過。我的分頁功能比較容易讓使用者誤解,用戶通常認(rèn)為分頁歸并會(huì)占用大量內(nèi)存。用戶非常容易產(chǎn)生我會(huì)將大量無意義的數(shù)據(jù)加載至內(nèi)存中,造成內(nèi)存溢出風(fēng)險(xiǎn)的錯(cuò)覺。其實(shí),通過流式歸并的原理可知,會(huì)將數(shù)據(jù)全部加載到內(nèi)存中的只有內(nèi)存分組歸并這一種情況。除了內(nèi)存分組歸并這種情況之外,其他情況都可以通過流式歸并獲取數(shù)據(jù)結(jié)果集,因此我會(huì)通過結(jié)果集的 next 方法將無需取出的數(shù)據(jù)全部跳過,并不會(huì)將其存入內(nèi)存。
但同時(shí)需要注意的是,由于排序的需要,大量的數(shù)據(jù)仍然需要傳輸?shù)轿宜?JVM 的內(nèi)存空間(只不過我丟掉無用的數(shù)據(jù),如上段所述)。因此,采用 LIMIT 這種方式分頁,并非最佳實(shí)踐。由于 LIMIT 并不能通過索引查詢數(shù)據(jù),因此如果可以保證 ID 的連續(xù)性,通過 ID 進(jìn)行分頁是比較好的解決方案,例如:
SELECT?*?FROM?t_order?WHERE?id?>?100000?AND?id?<=?100010?ORDER?BY?id;
或通過記錄上次查詢結(jié)果的最后一條記錄的 ID 進(jìn)行下一頁的查詢,例如:
SELECT?*?FROM?t_order?WHERE?id?>?10000000?LIMIT?10;
5. 結(jié)束語
我是 Sharding-JDBC,一個(gè)數(shù)據(jù)庫水平分片中間件。當(dāng)你們把邏輯 SQL 交給我處理時(shí),作為中間件,我把 SQL 解析、路由、改寫、執(zhí)行、歸并的復(fù)雜工作統(tǒng)統(tǒng)對你們屏蔽了。而你們要做的就是執(zhí)行數(shù)據(jù)庫和數(shù)據(jù)表水平拆分(無論是手動(dòng)拆分還是自動(dòng)化拆分均可,不過拆分是你們的工作,不是我的)、實(shí)現(xiàn)我提供的分片算法接口,告訴我怎么根據(jù)分片鍵的值找到對應(yīng)的分片、在配置文件或者配置 API 中描述分片策略。
再讓你們看一眼我提供的各種 ShardingAlgorithm 接口中的 doSharding()方法吧,這是你們使用我時(shí)接觸得最多的一個(gè)方法,這也是你們使用我時(shí)唯一需要?jiǎng)幽X筋的地方:
/**
*?所有的分片算法?interface?都包含該方法
*
*?@param?所有可能的分片表(或分片庫)名稱
*?@param?分片鍵的值
*?@return?根據(jù)分片鍵的值,找到對應(yīng)的分片表(或分片庫)名稱并返回
*/
Collection?doSharding(
????Collection?availableTargetNames,?
????ComplexKeysShardingValue?shardingValue
) ;
我并非法力無邊,我還有很多局限。在單片路由和多片路由的場景下,我全面支持 DML、DDL、DCL、TCL 和部分 DAL,支持分頁、去重、排序、分組、聚合、不跨數(shù)據(jù)庫的關(guān)聯(lián)查詢等操作。但在多片路由的場景下,我不支持 HAVING、UNION 等操作,對子查詢的支持也有限。其他種種細(xì)節(jié),一篇文章,難以詳述。
我是 Sharding-JDBC,關(guān)于我的基本用法和基本原理,我說完了,你禿了嗎?
