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

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

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

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

如果你連讀這段工作流程都很困難,那你就能明白我在這個(gè)水平分片的世界里有多辛苦。關(guān)于這段工作流程,我會(huì)在后文慢慢說(shuō)給你聽(tīng)。
3. 召喚我的方式
我是 Sharding-JDBC,我被定位為一個(gè)輕量級(jí)數(shù)據(jù)庫(kù)中間件,當(dāng)你們召喚我去統(tǒng)治水平拆分后的數(shù)據(jù)庫(kù)和數(shù)據(jù)表時(shí),只需要做下面幾件事:
1)引入依賴(lài)包。
maven 是統(tǒng)治依賴(lài)包世界的神,在他誕生之后,一切對(duì) jar 包的引用就變得簡(jiǎn)單了。向 maven 獲取我的 jar 包,咒語(yǔ)是:
<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)治對(duì)象世界的神,Springboot 繼承了 Spring 的統(tǒng)治法則,并簡(jiǎn)化了 Spring 的配置),那么就可以向 maven 獲取我的 springboot starter jar 包,咒語(yǔ)是:
<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ù)庫(kù)和數(shù)據(jù)表的。你們可以通過(guò)我提供的 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è)子庫(kù)的策略
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è)子庫(kù)的算法
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 |
|---|---|---|---|---|
這段配置代碼描述了對(duì) t_order 表進(jìn)行的如下圖所示的數(shù)據(jù)表水平分片(對(duì) t_order_item 表也要進(jìn)行類(lèi)似的水平分片,但是這部分配置省略了):

在這段配置中,或許你們注意到了一些奇怪的表達(dá)式:
ds$->{0..1}.t_order$->{0..1}
ds_${user_id?%?2}
t_order_${order_id?%?2}
這些表達(dá)式被稱(chēng)為 Groovy 表達(dá)式,它們的含義很容易識(shí)別:
1)對(duì) t_order 進(jìn)行兩種維度的拆分:數(shù)據(jù)庫(kù)維度和表維度數(shù);
2)在數(shù)據(jù)庫(kù)維度,t_order.user_id % 2 == 0 的記錄全部落到 ds0,t_order.user_id % 2 == 1 的記錄全部落到 ds1;(有人稱(chēng)這一過(guò)程為水平分庫(kù),其實(shí)它的本質(zhì)還是在水平地分表,只不過(guò)依據(jù)表中 user_id 的不同把拆分的后的表放入兩個(gè)數(shù)據(jù)庫(kù)實(shí)例。)
3)在表維度,t_order.order_id% 2 == 0 的記錄全部落到 t_order0,t_order.order_id% 2 == 1 的記錄全部落到 t_order1。
4)對(duì)記錄的讀和寫(xiě)都按照這種方向進(jìn)行,“方向”,就是分片方式,就是路由。
我允許你們這些工程師使用這種簡(jiǎn)潔的 Groovy 表達(dá)式告訴我你們?cè)O(shè)置的分片策略和分片算法。但是這種方式所能表達(dá)的含義是有限的。因此,我提供了分片策略接口和分片算法接口讓你們利用 Java 代碼盡情表達(dá)更為復(fù)雜的分片策略和分片算法。關(guān)于這一點(diǎn),我將在《我的特性和工作方法》這一章詳述。
而且在這里我要先告訴你,分片算法是分片策略的組成部分,分片策略設(shè)置=分片鍵設(shè)置+分片算法設(shè)置。上述配置里使用的策略是 Inline 類(lèi)型的分片策略,使用的算法是 Inline 類(lèi)型的行表達(dá)式算法,你或許不清楚我現(xiàn)在講的這些術(shù)語(yǔ),不要著急,我會(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è)子庫(kù)的策略
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è)子庫(kù)的算法
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}
這段配置文件的語(yǔ)義和上面的 Java 配置代碼同義。
3)創(chuàng)建數(shù)據(jù)源。
若使用上文所示的 Java API 進(jìn)行配置,則可以通過(guò) ShardingSphereDataSourceFactory 工廠(chǎng)創(chuàng)建數(shù)據(jù)源,該工廠(chǎng)產(chǎn)生一個(gè) ShardingSphereDataSource 實(shí)例,ShardingSphereDataSource 實(shí)現(xiàn)自 JDBC 的標(biāo)準(zhǔn)接口 DataSource(所以 ShardingSphereDataSource 實(shí)例也是接口 DataSource 的實(shí)例)。之后,就可以通過(guò) dataSource 調(diào)用原生 JDBC 接口來(lái)執(zhí)行 SQL 查詢(xún),或者將 dataSource 配置到 JPA,MyBatis 等 ORM 框架來(lái)執(zhí)行 SQL 查詢(xún)。
//?創(chuàng)建?ShardingSphereDataSource
DataSource?dataSource?=?ShardingSphereDataSourceFactory.createDataSource(
????dataSourceMap,?
????Collections.singleton(shardingRuleConfig,?new?Properties())
);
若使用上文所示的基于 springboot starter 的 properties 配置文件進(jìn)行分片配置,則可以直接通過(guò) Spring 提供的自動(dòng)注入的方式獲得數(shù)據(jù)源實(shí)例 dataSource(同樣,這也是一個(gè) ShardingSphereDataSource 實(shí)例)。之后,就可以通過(guò) dataSource 調(diào)用原生 JDBC 接口來(lái)執(zhí)行 SQL 查詢(xún),或者將 dataSource 配置到 JPA,MyBatis 等 ORM 框架來(lái)執(zhí)行 SQL 查詢(xún)。
/**
*?注入一個(gè)?ShardingSphereDataSource?實(shí)例
*/
@Resource
private?DataSource?dataSource;
有了 dataSource(以上兩種方式產(chǎn)生的 dataSource 沒(méi)有區(qū)別,都是 ShardingSphereDataSource 的一個(gè)實(shí)例,業(yè)務(wù)代碼將 SQL 交給這個(gè) dataSource,也就是交到了我的手中),就可以執(zhí)行 SQL 查詢(xún)了。
4)執(zhí)行 SQL。這里給出 dataSource 調(diào)用原生 JDBC 接口來(lái)執(zhí)行 SQL 查詢(xún)的示例:
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í),只感覺(jué)自己在對(duì)一個(gè)邏輯庫(kù)中的兩個(gè)邏輯表進(jìn)行關(guān)聯(lián)查詢(xún),并沒(méi)有意識(shí)到物理分片的存在。而背后是我在進(jìn)行 SQL 語(yǔ)句的解析、路由、改寫(xiě)、執(zhí)行和結(jié)果歸并!
4. 我的特性和我的工作方法
4.2. 一些核心概念
我是 Sharding-JDBC,我是統(tǒng)治水平分片世界的神,我要向你們解釋我的特性和治理方法。在此之前,我要給出一系列用于描述我的術(shù)語(yǔ)。
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ù)器將針對(duì)邏輯表編寫(xiě)的 SQL 交給我,我在執(zhí)行前,要找到 SQL 語(yǔ)句里包含的查詢(xún)條件(where ......)所對(duì)應(yīng)的分片(物理表),然后再針對(duì)這些分片進(jìn)行查詢(xún),這個(gè)找分片的過(guò)程叫做路由。
而怎樣找分片,是由你們?cè)诜制呗灾懈嬖V我的。
4.2.4. 分片策略和分片算法
在上文的配置示例中,有如下的一段:
......
//?配置?t_order?被拆分到多個(gè)子庫(kù)的策略
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è)子庫(kù)的算法
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á)的就是對(duì) t_order 表進(jìn)行的分片策略和分片算法的配置。
上文說(shuō)到,我允許你們這些工程師使用簡(jiǎn)潔的 Groovy 表達(dá)式告訴我你們?cè)O(shè)置的分片策略和分片算法。但是這種方式所能表達(dá)的含義是有限的。因此,我提供了分片策略接口和分片算法接口讓你們利用靈活的 Java 代碼盡情表達(dá)更為復(fù)雜的分片策略和分片算法。
所謂分片策略,就是分片鍵和分片算法的組合,由于分片算法的獨(dú)立性,我將其獨(dú)立抽離出來(lái),由你們自己實(shí)現(xiàn),也就是告訴我數(shù)據(jù)是怎么根據(jù)分片鍵的值找到對(duì)應(yīng)的分片,進(jìn)而對(duì)這些分片執(zhí)行 SQL 查詢(xún)。
當(dāng)然我也提供了一些內(nèi)置的簡(jiǎn)單算法的實(shí)現(xiàn)。上面提到的基于 Groovy 表達(dá)式的分片算法就是我內(nèi)置的一種算法實(shí)現(xiàn),你們只要給我一段語(yǔ)義準(zhǔn)確無(wú)誤的 Groovy 表達(dá)式,我就能知道怎么根據(jù)分片鍵的值找到對(duì)應(yīng)的分片。
我的分片策略有兩個(gè)維度,如下圖所示,分別是數(shù)據(jù)源分片策略(databaseShardingStrategy)和表分片策略(tableShardingStrategy)。數(shù)據(jù)源分片策略表示數(shù)據(jù)被路由到目標(biāo)物理數(shù)據(jù)庫(kù)的策略,表分片策略表示數(shù)據(jù)被路由到目標(biāo)物理表的策略。表分片策略是依賴(lài)于數(shù)據(jù)源分片策略的,也就是說(shuō)要先分庫(kù)再分表,當(dāng)然也可以只分表。

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


一、標(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(
????????//?所有可能的分片表(或分片庫(kù))名稱(chēng)
????????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ù)分片鍵的值,找到對(duì)應(yīng)的分片表(或分片庫(kù))名稱(chēng)并返回
????????return?result;
????}
????
????/**
????*?處理范圍分片
????*/
????@SuppressWarnings("unchecked")
????private?Collection?doSharding(
????????//?所有可能的分片表(或分片庫(kù))名稱(chēng)
????????final?Collection?availableTargetNames,?
????????//?分片鍵的值
????????final?RangeRouteValue>?shardingValue
????)? {
????????......
????????//?調(diào)用?rangeShardingAlgorithm.doSharding()根據(jù)分片鍵的值找到對(duì)應(yīng)的
????????//?分片表(或分片庫(kù))名稱(chēng)并返回,rangeShardingAlgorithm.doSharding()
????????//?由你們自己實(shí)現(xiàn)
????????return?rangeShardingAlgorithm.doSharding(
????????????availableTargetNames,?
????????????new?RangeShardingValue(
????????????????shardingValue.getTableName(),?
????????????????shardingValue.getColumnName(),?
????????????????shardingValue.getValueRange()
????????????)
????????);
????}
????
????/**
????*?處理精確分片
????*/
????@SuppressWarnings("unchecked")
????private?Collection?doSharding(
????????//?所有可能的分片表(或分片庫(kù))名稱(chēng)
????????final?Collection?availableTargetNames,?
????????//?分片鍵的值
????????final?ListRouteValue>?shardingValue
????)? {
????????Collection?result?=?new?LinkedList<>();
????????for?(Comparable>?each?:?shardingValue.getValues())?{
????????????//?調(diào)用?preciseShardingAlgorithm.doSharding()根據(jù)分片鍵的值找到對(duì)應(yīng)的
????????????//?分片表(或分片庫(kù))名稱(chē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?所有可能的分片表(或分片庫(kù))名稱(chēng)
?????*?@param?分片鍵的值
?????*?@return?根據(jù)分片鍵的值,找到對(duì)應(yīng)的分片表(或分片庫(kù))名稱(chēng)并返回
?????*/
????String?doSharding(
????????Collection?availableTargetNames,?
????????PreciseShardingValue?shardingValue
????) ;
}
package?org.apache.shardingsphere.api.sharding.standard;
......
public?interface?RangeShardingAlgorithm<T?extends?Comparable>>?
????extends?ShardingAlgorithm?{
????
????/**
?????*?@param?所有可能的分片表(或分片庫(kù))名稱(chēng)
?????*?@param?分片鍵的值
?????*?@return?根據(jù)分片鍵的值,找到對(duì)應(yīng)的分片表(或分片庫(kù))名稱(chēng)并返回
?????*/
????Collection?doSharding(
????????Collection?availableTargetNames,?
????????RangeShardingValue?shardingValue
????) ;
}
標(biāo)準(zhǔn)分片策略提供對(duì) SQL 語(yǔ)句中的操作符 =、>、 <、>=、<=、IN 和 BETWEEN AND 的分片操支持。
標(biāo)準(zhǔn)分片策略只支持單分片鍵,例如對(duì) t_order 表只根據(jù) order_id 分片。標(biāo)準(zhǔn)分片策略提供 PreciseShardingAlgorithm(接口)和 RangeShardingAlgorithm(接口)兩個(gè)分片算法。PreciseShardingAlgorithm(接口)顧名思義用于處理操作符=和 IN 的精確分片。RangeShardingAlgorithm (接口)顧名思義用于處理操作符 BETWEEN AND、>、<、>=、<= 的范圍分片。
我舉個(gè)例子幫助你理解以上兩段話(huà)的含義。以 t_order 為例,假如你使用 order_id 作為 t_order 的分片鍵,并設(shè)計(jì)了以下的分片策略:
策略一:設(shè)置 6 個(gè)分片
t_order.order_id?%?6?==?0?的查詢(xún)分片到?t_order0
t_order.order_id?%?6?==?1?的查詢(xún)分片到?t_order1
t_order.order_id?%?6?==?2?的查詢(xún)分片到?t_order2
t_order.order_id?%?6?==?3?的查詢(xún)分片到?t_order3
t_order.order_id?%?6?==?4?的查詢(xún)分片到?t_order4
t_order.order_id?%?6?==?5?的查詢(xún)分片到?t_order5
策略二:設(shè)置 2 個(gè)分片
t_order.order_id?%?6?in?(0,2,4)?的查詢(xún)分片到?t_order1
t_order.order_id?%?6?in?(1,3,5)?的查詢(xún)分片到?t_order1
策略三:經(jīng)過(guò)估算訂單不超過(guò) 60000?個(gè),設(shè)置 6 個(gè)分片
t_order.order_id?between?0?and?10000?的查詢(xún)分片到?t_order0
t_order.order_id?between?10000?and?20000?的查詢(xún)分片到?t_order1
t_order.order_id?between?20000?and?30000?的查詢(xún)分片到?t_order2
t_order.order_id?between?30000?and?40000?的查詢(xún)分片到?t_order3
t_order.order_id?between?40000?and?50000?的查詢(xún)分片到?t_order4
t_order.order_id?between?50000?and?60000?的查詢(xún)分片到?t_order5
策略四:經(jīng)過(guò)估算訂單不超過(guò) 20000?個(gè),設(shè)置 2 個(gè)分片
t_order.order_id?<=10000?的查詢(xún)分片到?t_order0
t_order.order_id?>10000?的查詢(xún)分片到?t_order1
......
那你就可以把以下三項(xiàng):
1)分片鍵 order_id
2)描述以上分片策略?xún)?nèi)容的 PreciseShardingAlgorithm(接口)的實(shí)現(xiàn)類(lèi)或 RangeShardingAlgorithm(接口)的實(shí)現(xiàn)類(lèi)
3)前兩項(xiàng)(即分片策略)的作用目標(biāo) t_order 表
寫(xiě)到分片配置里(無(wú)論是通過(guò)配置 API 還是通過(guò)配置文件),那我就能知道如何去路由 SQL,即根據(jù)分片鍵的值,找到對(duì)應(yīng)的分片表(或分片庫(kù))。
有了這些配置,我就能幫你們透明處理如下 SQL 語(yǔ)句,不管實(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(
????????//?所有可能的分片表(或分片庫(kù))名稱(chēng)
????????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ù)分片鍵的值找到對(duì)應(yīng)的
????????????//?分片表(或分片庫(kù))名稱(chē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ù)分片鍵的值,找到對(duì)應(yīng)的分片表(或分片庫(kù))名稱(chēng)并返回
????????return?result;
????}
}
其中 ComplexKeysShardingAlgorithm(接口)的源代碼為:
package?org.apache.shardingsphere.api.sharding.complex;
......
public?interface?ComplexKeysShardingAlgorithm<T?extends?Comparable>>?
????extends?ShardingAlgorithm?{
????
????/**
?????*?@param?所有可能的分片表(或分片庫(kù))名稱(chēng)
?????*?@param?分片鍵的值
?????*?@return?根據(jù)分片鍵的值,找到對(duì)應(yīng)的分片表(或分片庫(kù))名稱(chēng)并返回
?????*/
????Collection?doSharding(
????????Collection?availableTargetNames,?
????????ComplexKeysShardingValue?shardingValue
????) ;
}
復(fù)合分片策略提供對(duì) SQL 語(yǔ)句中的操作符 =、>、<、>=、<=、IN 和 ETWEEN AND 的分片操作支持。
復(fù)合分片策略支持多分片鍵,例如對(duì) t_order 表根據(jù) order_id 和 user_id 分片。復(fù)合分片策略提供 ComplexKeysShardingAlgorithm(接口)分片算法。
我舉個(gè)例子幫助你理解以上兩段話(huà)的含義。以 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?的查詢(xún)分片到?t_order0
t_order.order_id?%?2?==?0?&&?t_order.user_id?%?2?==?1?的查詢(xún)分片到?t_order1
t_order.order_id?%?2?==?1?&&?t_order.user_id?%?2?==?0?的查詢(xún)分片到?t_order2
t_order.order_id?%?2?==?1?&&?t_order.user_id?%?2?==?1?的查詢(xún)分片到?t_order3
策略二:經(jīng)過(guò)估算訂單不超過(guò) 60000?個(gè)、用戶(hù)不超過(guò) 1000?個(gè),設(shè)置 4 個(gè)分片
t_order.order_id?between?0?and?40000?&&?t_order.user_id?between?0?and?500?的查詢(xún)分片到?t_order0
t_order.order_id?between?0?and?40000?&&?t_order.user_id?between?500?and?1000?的查詢(xún)分片到?t_order1
t_order.order_id?between?40000?and?60000?&&?t_order.user_id?between?0?and?500?的查詢(xún)分片到?t_order2
t_order.order_id?between?40000?and?60000?&&?t_order.user_id?between?500?and?1000?的查詢(xún)分片到?t_order3
......
那你就可以把以下三項(xiàng):
1)分片鍵 order_id 和 user_id
2)描述以上分片策略?xún)?nèi)容的 ComplexKeysShardingAlgorithm(接口)的實(shí)現(xiàn)類(lèi)
3)前兩項(xiàng)(即分片策略)的作用目標(biāo) t_order 表
寫(xiě)到分片配置里(無(wú)論是通過(guò)配置 API 還是通過(guò)配置文件),那我就能知道如何去路由 SQL,即根據(jù)分片鍵的值,找到對(duì)應(yīng)的分片表(或分片庫(kù))。
有了這些配置,我就能幫你們透明處理如下 SQL 語(yǔ)句,不管實(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 對(duì) t_order 進(jìn)行水平拆分(到不同的子庫(kù)),再依照 order_id % 2 對(duì) t_order 進(jìn)行水平拆分(到不同的子表)。但這并不是說(shuō)使用了復(fù)合分片策略,而是使用了兩個(gè)兩個(gè)維度的標(biāo)準(zhǔn)分片策略。兩個(gè)維度,分別是數(shù)據(jù)源分片策略(DatabaseShardingStrategy)和表分片策略(TableShardingStrategy),且在數(shù)據(jù)源分片策略上使用 user_id 作為單分片鍵、在表分片策略上使用 order_id 作為單分片鍵。
三、Hint(翻譯為暗示) 分片策略
Hint 分片策略對(duì)應(yīng) HintShardingStrategy 這個(gè) final class,同標(biāo)準(zhǔn)分片策略和符合分片策略的代碼類(lèi)似,HintShardingStrategy 中包含一個(gè) HintShardingAlgorithm 接口的實(shí)例,并調(diào)用它的 doSharding()方法。你們要自己去實(shí)現(xiàn)這個(gè) HintShardingAlgorithm 接口中的 doSharding()方法,這樣我就能知道如何根據(jù)分片鍵的值,找到對(duì)應(yīng)的分片表(或分片庫(kù))。此處不在展示 HintShardingStrategy 和 HintShardingAlgorithm 的源碼。
Hint 分片策略是指通過(guò) Hint 指定分片值而非從 SQL 中提取分片值的方式進(jìn)行分片的策略。簡(jiǎn)單來(lái)講就是我收到的 SQL 語(yǔ)句中不包含分片值(像上面給出的幾段 SQL 就是包含分片值的 SQL),但是工程師會(huì)通過(guò)我提供的 Java API 將分片值暗示給我,這樣我就知道怎樣路由 SQL 查詢(xún)到具體的分片了。就像下面這樣:
String?sql?=?"SELECT?*?FROM?t_order";
try?(
????//?HintManager?是使用“暗示”的工具,它會(huì)把暗示的分片值放入
????//?當(dāng)前線(xiàn)程上下文(ThreadLocal)中,這樣當(dāng)前線(xiàn)程執(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())?{
?????????//...
????????}?
????}?
}
四、不分片策略
對(duì)應(yīng) NoneShardingStrategy,這是一個(gè) final class。由于我并不要求所有的表(或庫(kù))都進(jìn)行水平分片,因此當(dāng)工程師要通過(guò)我執(zhí)行對(duì)不分片表(或庫(kù))的 SQL 查詢(xún)時(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(
????????//?所有可能的分片表(或分片庫(kù))名稱(chēng)
????????final?Collection?availableTargetNames,?
????????//?分片鍵的值
????????final?Collection?shardingValues,?
????????final?ConfigurationProperties?properties
????)? {
????????
????????//?不需要任何算法,不進(jìn)行任何邏輯處理,直接返回
????????//?所有可能的分片表(或分片庫(kù))名稱(chēng)
????????return?availableTargetNames;
????}
}
五、Inline 分片策略
Inline 分片策略,也叫做行表達(dá)式分片策略。Inline 分片策略對(duì)應(yīng) InlineShardingStrategy。Inline 分片策略是為用 Grovvy 表達(dá)式描述的分片算法準(zhǔn)備的分片策略。文章開(kāi)始展示的兩段配置中就使用了 Inline 分片策略。InlineShardingStrategy 把 Grovvy 表達(dá)式當(dāng)做分片算法的實(shí)現(xiàn),因此 HintShardingStrategy 中不包含算法域變量,這一點(diǎn)有別于 StandardShardingStrategy 等 final class。這里不再展示 InlineShardingStrategy 的源碼。
我知道,這段關(guān)于分片策略和分片算法的表述很難理解。不過(guò)我還是想讓你們明白,無(wú)論對(duì)某個(gè)邏輯表(或庫(kù))進(jìn)行怎樣的分片策略配置,這些策略不過(guò)都是在告訴我怎樣處理分片,也就是告訴我如何根據(jù)分片鍵的值,找到對(duì)應(yīng)的分片表(或分片庫(kù))。只不過(guò)我的創(chuàng)造者把這個(gè)簡(jiǎn)單的過(guò)程翻出了很多花樣,也就是你們?cè)谏厦婵吹降母鞣N策略,以提供使用上的靈活性。
4.2.5. 綁定表
指分片規(guī)則一致的主表和子表。例如 t_order 是主表,存儲(chǔ)訂單的基本信息;t_order_item 是子表,存儲(chǔ)訂單中的商品和價(jià)格明細(xì)。若兩張表均按照 order_id 分片,并且配置了兩個(gè)表之間的綁定關(guān)系,則此兩張表互為綁定表。綁定表之間的多表關(guān)聯(lián)查詢(xún)不會(huì)出現(xiàn)笛卡爾積關(guān)聯(lián),關(guān)聯(lián)查詢(xún)效率將大大提升。舉例說(shuō)明,如果 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 配置文件中可以這么寫(xiě):
# 設(shè)置綁定表
sharding.jdbc.config.sharding.binding-tables=t_order, t_order_item
4.3. 我處理 SQL 的過(guò)程
我是 Sharding-JDBC,我是水平分片世界的神。我的職責(zé)是透明化水平分庫(kù)分表所帶來(lái)的影響,讓使用方盡量像使用一個(gè)數(shù)據(jù)庫(kù)一樣使用水平分片之后的數(shù)據(jù)庫(kù)集群,或者像使用一個(gè)數(shù)據(jù)表一樣使用水平分片之后的數(shù)據(jù)表。
我的法力,來(lái)源于我的創(chuàng)造者為我設(shè)計(jì)的內(nèi)核,它把 SQL 語(yǔ)句的處理分成了 SQL 解析 =>SQL 路由 => SQL 改寫(xiě) => SQL 執(zhí)行 => 結(jié)果歸并五個(gè)主要流程。

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

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

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

通過(guò)圖中所示,想要取得兩個(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é)果歸并無(wú)論怎么實(shí)現(xiàn),都不可能獲得正確的結(jié)果。
正確的做法是將分頁(yè)條件改寫(xiě)為 LIMIT 0, 3,取出所有前兩頁(yè)數(shù)據(jù),再結(jié)合排序條件計(jì)算出正確的數(shù)據(jù)。即:
SELECT?age?FROM?t_user?ORDER?BY?age?DESC?LIMIT?0,?3;
路由并改寫(xiě)之后的結(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 改寫(xiě)之后的分頁(yè)執(zhí)行結(jié)果:

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

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

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

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

下圖則展現(xiàn)了進(jìn)行 next 調(diào)用的時(shí)候,排序歸并是如何進(jìn)行的。通過(guò)下圖你們可以看到,當(dāng)進(jìn)行第一次 next 調(diào)用時(shí),排在隊(duì)列首位的 t_user0 將會(huì)被彈出隊(duì)列,并且將當(dāng)前游標(biāo)指向的數(shù)據(jù)值(也就是 100)返回至查詢(xún)客戶(hù)端,并且將游標(biāo)下移一位之后,重新放入優(yōu)先級(jí)隊(duì)列。而優(yōu)先級(jí)隊(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)指向的值返回至客戶(hù)端,并下移游標(biāo),繼續(xù)加入隊(duì)列排隊(duì),以此類(lèi)推。當(dāng)一個(gè)結(jié)果集中已經(jīng)沒(méi)有數(shù)據(jù)了,則無(wú)需再次加入隊(duì)列。
可以看到,對(duì)于每個(gè)數(shù)據(jù)結(jié)果集中的數(shù)據(jù)有序,而多數(shù)據(jù)結(jié)果集整體無(wú)序的情況下,我無(wú)需將所有的數(shù)據(jù)都加載至內(nèi)存即可排序,我使用的是流式歸并的方式,每次 next 僅獲取唯一正確的一條數(shù)據(jù),極大的節(jié)省了內(nèi)存的消耗。
3)分組歸并
分組歸并的情況最為復(fù)雜,它分為流式分組歸并和內(nèi)存分組歸并。流式分組歸并要求 SQL 的排序項(xiàng)與分組項(xiàng)的字段必須保持一致,否則只能通過(guò)內(nèi)存歸并才能保證其數(shù)據(jù)的正確性。
舉例說(shuō)明,假設(shè)邏輯表 t_socre(表結(jié)構(gòu)中包含考生的姓名 name、科目 subject 和分?jǐn)?shù) score,且為了簡(jiǎn)單起見(jiàn),不考慮重名的情況)根據(jù)科目分成 3 片:t_socre_java、t_socre_go、t_socre_python(后文插圖中的分片表均未展示科目字段,只展示姓名和分?jǐn)?shù)字段)?,F(xiàn)在要通過(guò) SQL 獲取每位考生的總分:
SELECT?name,?SUM(score)?as?sum_score?FROM?t_score?GROUP?BY?name?ORDER?BY?name?asc;
以上 SQL 被路由和改寫(xiě)之后的結(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í),過(guò)程與排序歸并類(lèi)似。下圖展現(xiàn)了進(jìn)行 next 調(diào)用的時(shí)候,流式分組歸并是如何進(jìn)行的。

通過(guò)上一張圖你們可以看到,當(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ì)列的前列。
對(duì)于分組項(xiàng)與排序項(xiàng)不一致的情況,由于在每個(gè)分片結(jié)果集中分組字段的值并非有序的,因此無(wú)法使用流式歸并,需要將所有的分片結(jié)果集數(shù)據(jù)加載至內(nèi)存中進(jìn)行分組和聚合。例如,若通過(guò)以下 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ù)如下圖所示,顯然是無(wú)法像上圖那樣進(jìn)行流式歸并的,不信你按照上一張圖的過(guò)程動(dòng)筆畫(huà)一下試試 :-)

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

通過(guò)下一張圖你們可以看到,當(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ì)列的前列。
顯然,這一過(guò)程屬于流式歸并。

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