0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫(xiě)文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

Sharding-JDBC 基本使用方法

數(shù)據(jù)分析與開(kāi)發(fā) ? 來(lái)源:數(shù)據(jù)分析與開(kāi)發(fā) ? 作者:碼海-郭可巖 ? 2020-11-19 15:54 ? 次閱讀

前言

這是一篇將“介紹 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、注解的效果之后,就妄稱自己學(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)知。為了使這些文字盡量擺脫枯燥的味道,文章采用了第一人稱的講述方式,讓 Sharding-JDBC 現(xiàn)身說(shuō)法,進(jìn)行自我剖析,希望給大家一個(gè)更好的閱讀體驗(yàn)。

但是,妄圖不動(dòng)腦子就能對(duì)某項(xiàng)技術(shù)產(chǎn)生深度認(rèn)知是絕不可能的,你思考得越多,你得到的越多。這就印證了那句話:“我變禿了,也變強(qiáng)了?!?/p>

1. 我的出生和我的家族

我是 Sharding-JDBC,一個(gè)關(guān)系型數(shù)據(jù)庫(kù)中間件,我的全名是 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ù)庫(kù)節(jié)點(diǎn),在性能、可用性和運(yùn)維成本這三方面已經(jīng)難于滿足互聯(lián)網(wǎng)的海量數(shù)據(jù)場(chǎng)景。

從性能方面來(lái)說(shuō),由于關(guān)系型數(shù)據(jù)庫(kù)大多采用 B+樹(shù)類型的索引,在數(shù)據(jù)量逐漸增大的情況下,索引深度的增加也將使得磁盤(pán)訪問(wèn)的 IO 次數(shù)增加,進(jìn)而導(dǎo)致查詢性能的下降;同時(shí),高并發(fā)訪問(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ù)查詢請(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ú)法滿足互聯(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ā)查詢請(qǐng)求的場(chǎng)景,而不是要顛覆關(guān)系型數(shù)據(jù)庫(kù)。

分庫(kù)分表方案就是這種增量,它的誕生解決了海量數(shù)據(jù)存儲(chǔ)和高并發(fā)查詢請(qǐng)求的問(wèn)題。

但是,單一數(shù)據(jù)庫(kù)被分庫(kù)分表之后,繁雜的庫(kù)和表使得編寫(xiě)持久層代碼的工程師的思維負(fù)擔(dān)翻了很多倍,他們需要考慮一個(gè)業(yè)務(wù) SQL 應(yīng)該去哪個(gè)庫(kù)的哪個(gè)表里去查詢,查詢到的結(jié)果還要進(jìn)行聚合,如果遇到多表關(guān)聯(lián)查詢、排序、分頁(yè)、事務(wù)等等問(wèn)題,那簡(jiǎn)直是一個(gè)噩夢(mèng)。

于是我們創(chuàng)造了你。你可以讓工程師們以像查詢單數(shù)據(jù)庫(kù)實(shí)例和單表那樣來(lái)查詢被水平分割的庫(kù)和表,我們稱之為透明查詢。

你是水平分片世界的神。

這使我感到驕傲。

我被定位為一個(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ù)的訪問(wèn)。通過(guò)無(wú)中心、零侵?的?案提供與數(shù)據(jù)庫(kù)交互的的嚙合層,即 Database Mesh,又可稱數(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ù)拆分的方式稱為垂直分片,又稱為縱向拆分,它的核心理念是專庫(kù)專用。在拆分之前,一個(gè)數(shù)據(jù)庫(kù)由多個(gè)數(shù)據(jù)表構(gòu)成,每個(gè)表對(duì)應(yīng)著不同的業(yè)務(wù)。而拆分之后,則是按照業(yè)務(wù)將表進(jìn)行歸類,分布到不同的數(shù)據(jù)庫(kù)中,從而將壓力分散至不同的數(shù)據(jù)庫(kù)。下圖展示了根據(jù)業(yè)務(wù)需要,將用戶表和訂單表垂直分片到不同的數(shù)據(jù)庫(kù)的方案。

031

垂直分片往往需要對(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)一步處理。

水平分片又稱為橫向拆分。相對(duì)于垂直分片,它不再將數(shù)據(jù)根據(jù)業(yè)務(wù)邏輯分類,而是通過(guò)某個(gè)字段(或某幾個(gè)字段),根據(jù)某種規(guī)則將數(shù)據(jù)分散至多個(gè)庫(kù)或表中,每個(gè)分片僅包含數(shù)據(jù)的一部分。例如:根據(jù)主鍵分片,偶數(shù)主鍵的記錄放入 0 庫(kù)(或表),奇數(shù)主鍵的記錄放入 1 庫(kù)(或表),如下圖所示。

032

水平分片從理論上突破了單機(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)造的幻象。

033

而為了營(yíng)造這種幻象,我在幕后付出了很多。

當(dāng)一個(gè) Java 應(yīng)用服務(wù)器節(jié)點(diǎn)將一個(gè)查詢 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ù)解析上下文匹配用戶對(duì)這句 SQL 所涉及的庫(kù)和表配置的分片策略(關(guān)于用戶配置的分片策略,我后文會(huì)慢慢解釋),并根據(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ò)多線程執(zhí)行器異步執(zhí)行路由和改寫(xiě)之后得到的 SQL 語(yǔ)句。

5)結(jié)果歸并:我將多個(gè)執(zhí)行結(jié)果集歸并以便于通過(guò)統(tǒng)一的 JDBC 接口輸出。

034

如果你連讀這段工作流程都很困難,那你就能明白我在這個(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)引入依賴包。

maven 是統(tǒng)治依賴包世界的神,在他誕生之后,一切對(duì) jar 包的引用就變得簡(jiǎn)單了。向 maven 獲取我的 jar 包,咒語(yǔ)是:

org.apache.shardingsphere shardingsphere-jdbc-core ${latest.release.version}

于是,我就出現(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ǔ)是:

org.apache.shardingsphere shardingsphere-jdbc-spring-boot-starter ${shardingsphere.version}

這樣,我就能和 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ù)源 MapdataSourceMap=newHashMap<>(); //配置第1個(gè)數(shù)據(jù)源 BasicDataSourcedataSource1=newBasicDataSource(); dataSource1.setDriverClassName("com.mysql.jdbc.Driver"); dataSource1.setUrl("jdbc//localhost:3306/ds0"); dataSource1.setUsername("root"); dataSource1.setPassword(""); dataSourceMap.put("ds0",dataSource1); //配置第2個(gè)數(shù)據(jù)源 BasicDataSourcedataSource2=newBasicDataSource(); dataSource2.setDriverClassName("com.mysql.jdbc.Driver"); dataSource2.setUrl("jdbc//localhost:3306/ds1"); dataSource2.setUsername("root"); dataSource2.setPassword(""); dataSourceMap.put("ds1",dataSource2); //配置t_order表規(guī)則 ShardingTableRuleConfigurationorderTableRuleConfig =newShardingTableRuleConfiguration( "t_order", "ds${0..1}.t_order${0..1}" ); //配置t_order被拆分到多個(gè)子庫(kù)的策略 orderTableRuleConfig.setDatabaseShardingStrategy( newStandardShardingStrategyConfiguration( "user_id", "dbShardingAlgorithm" ) ); //配置t_order被拆分到多個(gè)子表的策略 orderTableRuleConfig.setTableShardingStrategy( newStandardShardingStrategyConfiguration( "order_id", "tableShardingAlgorithm" ) ); //省略配置t_order_item表規(guī)則... //... //配置分片規(guī)則 ShardingRuleConfigurationshardingRuleConfig=newShardingRuleConfiguration(); shardingRuleConfig.getTables().add(orderTableRuleConfig); //配置t_order被拆分到多個(gè)子庫(kù)的算法 PropertiesdbShardingAlgorithmrProps=newProperties(); dbShardingAlgorithmrProps.setProperty( "algorithm-expression", "ds${user_id%2}" ); shardingRuleConfig.getShardingAlgorithms().put( "dbShardingAlgorithm", newShardingSphereAlgorithmConfiguration("INLINE",dbShardingAlgorithmrProps) ); //配置t_order被拆分到多個(gè)子表的算法 PropertiestableShardingAlgorithmrProps=newProperties(); tableShardingAlgorithmrProps.setProperty( "algorithm-expression", "t_order${order_id%2}" ); shardingRuleConfig.getShardingAlgorithms().put( "tableShardingAlgorithm", newShardingSphereAlgorithmConfiguration("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)行類似的水平分片,但是這部分配置省略了):

035

在這段配置中,或許你們注意到了一些奇怪的表達(dá)式:

ds$->{0..1}.t_order$->{0..1} ds_${user_id%2} t_order_${order_id%2}

這些表達(dá)式被稱為 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;(有人稱這一過(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 類型的分片策略,使用的算法是 Inline 類型的行表達(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//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//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 工廠創(chuàng)建數(shù)據(jù)源,該工廠產(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 查詢,或者將 dataSource 配置到 JPA,MyBatis 等 ORM 框架來(lái)執(zhí)行 SQL 查詢。

//創(chuàng)建ShardingSphereDataSource DataSourcedataSource=ShardingSphereDataSourceFactory.createDataSource( dataSourceMap, Collections.singleton(shardingRuleConfig,newProperties()) );

若使用上文所示的基于 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 查詢,或者將 dataSource 配置到 JPA,MyBatis 等 ORM 框架來(lái)執(zhí)行 SQL 查詢。

/** *注入一個(gè)ShardingSphereDataSource實(shí)例 */ @Resource privateDataSourcedataSource;

有了 dataSource(以上兩種方式產(chǎn)生的 dataSource 沒(méi)有區(qū)別,都是 ShardingSphereDataSource 的一個(gè)實(shí)例,業(yè)務(wù)代碼將 SQL 交給這個(gè) dataSource,也就是交到了我的手中),就可以執(zhí)行 SQL 查詢了。

4)執(zhí)行 SQL。這里給出 dataSource 調(diào)用原生 JDBC 接口來(lái)執(zhí)行 SQL 查詢的示例:

Stringsql="SELECTi.*FROMt_orderoJOINt_order_itemiONo.order_id=i.order_idWHEREo.user_id=?ANDo.order_id=?"; try( Connectionconn=dataSource.getConnection(); PreparedStatementps=conn.prepareStatement(sql) ){ ps.setInt(1,10); ps.setInt(2,1000); try( ResultSetrs=preparedStatement.executeQuery() ){ while(rs.next()){ //... } } }

在這個(gè)示例中,Java 代碼調(diào)用 dataSource 的 JDBC 接口時(shí),只感覺(jué)自己在對(duì)一個(gè)邏輯庫(kù)中的兩個(gè)邏輯表進(jìn)行關(guān)聯(liá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ǔ)句里包含的查詢條件(where ......)所對(duì)應(yīng)的分片(物理表),然后再針對(duì)這些分片進(jìn)行查詢,這個(gè)找分片的過(guò)程叫做路由。

而怎樣找分片,是由你們?cè)诜制呗灾懈嬖V我的。

4.2.4. 分片策略和分片算法

在上文的配置示例中,有如下的一段:

...... //配置t_order被拆分到多個(gè)子庫(kù)的策略 orderTableRuleConfig.setDatabaseShardingStrategy( newStandardShardingStrategyConfiguration( "user_id", "dbShardingAlgorithm" ) ); //配置t_order被拆分到多個(gè)子表的策略 orderTableRuleConfig.setTableShardingStrategy( newStandardShardingStrategyConfiguration( "order_id", "tableShardingAlgorithm" ) ); ...... ShardingRuleConfigurationshardingRuleConfig=newShardingRuleConfiguration(); shardingRuleConfig.getTables().add(orderTableRuleConfig); //配置t_order被拆分到多個(gè)子庫(kù)的算法 PropertiesdbShardingAlgorithmrProps=newProperties(); dbShardingAlgorithmrProps.setProperty( "algorithm-expression", "ds${user_id%2}" ); shardingRuleConfig.getShardingAlgorithms().put( "dbShardingAlgorithm", newShardingSphereAlgorithmConfiguration("INLINE",dbShardingAlgorithmrProps) ); //配置t_order被拆分到多個(gè)子表的算法 PropertiestableShardingAlgorithmrProps=newProperties(); tableShardingAlgorithmrProps.setProperty( "algorithm-expression", "t_order${order_id%2}" ); shardingRuleConfig.getShardingAlgorithms().put( "tableShardingAlgorithm", newShardingSphereAlgorithmConfiguration("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 查詢。

當(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)物理表的策略。表分片策略是依賴于數(shù)據(jù)源分片策略的,也就是說(shuō)要先分庫(kù)再分表,當(dāng)然也可以只分表。

036

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

我的 Jar 包源碼里的策略類和算法接口如下:

037

038

一、標(biāo)準(zhǔn)分片策略

標(biāo)準(zhǔn)分片策略 StandardShardingStrategy 的源代碼(部分)如下,這是一個(gè) final class。

packageorg.apache.shardingsphere.core.strategy.route.standard; ...... publicfinalclassStandardShardingStrategyimplementsShardingStrategy{ privatefinalStringshardingColumn; /** *要配合PreciseShardingAlgorithm或RangeShardingAlgorithm使用 *標(biāo)準(zhǔn)分片策略 */ privatefinalPreciseShardingAlgorithmpreciseShardingAlgorithm; privatefinalRangeShardingAlgorithmrangeShardingAlgorithm; publicStandardShardingStrategy( //傳入分片配置 finalStandardShardingStrategyConfigurationstandardShardingStrategyConfig ){ ...... //從配置中提取分片鍵 shardingColumn=standardShardingStrategyConfig.getShardingColumn(); //從配置中提取分片算法 preciseShardingAlgorithm=standardShardingStrategyConfig.getPreciseShardingAlgorithm(); rangeShardingAlgorithm=standardShardingStrategyConfig.getRangeShardingAlgorithm(); } @Override publicCollectiondoSharding( //所有可能的分片表(或分片庫(kù))名稱 finalCollectionavailableTargetNames, //分片鍵的值 finalCollectionshardingValues, finalConfigurationPropertiesproperties ){ RouteValueshardingValue=shardingValues.iterator().next(); CollectionshardingResult =shardingValueinstanceofListRouteValue //處理精確分片 ?doSharding(availableTargetNames,(ListRouteValue)shardingValue) //處理范圍分片 :doSharding(availableTargetNames,(RangeRouteValue)shardingValue); Collectionresult=newTreeSet<>(String.CASE_INSENSITIVE_ORDER); result.addAll(shardingResult); //根據(jù)分片鍵的值,找到對(duì)應(yīng)的分片表(或分片庫(kù))名稱并返回 returnresult; } /** *處理范圍分片 */ @SuppressWarnings("unchecked") privateCollectiondoSharding( //所有可能的分片表(或分片庫(kù))名稱 finalCollectionavailableTargetNames, //分片鍵的值 finalRangeRouteValueshardingValue ){ ...... //調(diào)用rangeShardingAlgorithm.doSharding()根據(jù)分片鍵的值找到對(duì)應(yīng)的 //分片表(或分片庫(kù))名稱并返回,rangeShardingAlgorithm.doSharding() //由你們自己實(shí)現(xiàn) returnrangeShardingAlgorithm.doSharding( availableTargetNames, newRangeShardingValue( shardingValue.getTableName(), shardingValue.getColumnName(), shardingValue.getValueRange() ) ); } /** *處理精確分片 */ @SuppressWarnings("unchecked") privateCollectiondoSharding( //所有可能的分片表(或分片庫(kù))名稱 finalCollectionavailableTargetNames, //分片鍵的值 finalListRouteValueshardingValue ){ Collectionresult=newLinkedList<>(); for(Comparableeach:shardingValue.getValues()){ //調(diào)用preciseShardingAlgorithm.doSharding()根據(jù)分片鍵的值找到對(duì)應(yīng)的 //分片表(或分片庫(kù))名稱并返回,preciseShardingAlgorithm.doSharding() //由你們自己實(shí)現(xiàn) Stringtarget =preciseShardingAlgorithm.doSharding( availableTargetNames, newPreciseShardingValue( shardingValue.getTableName(), shardingValue.getColumnName(), each ) ); if(null!=target){ result.add(target); } } returnresult; } /** *獲取所有的分片鍵 */ @Override publicCollectiongetShardingColumns(){ Collectionresult=newTreeSet<>(String.CASE_INSENSITIVE_ORDER); result.add(shardingColumn); returnresult; } }

其中 PreciseShardingAlgorithm(接口)和 RangeShardingAlgorithm(接口)的源代碼分別為:

packageorg.apache.shardingsphere.api.sharding.standard; ...... publicinterfacePreciseShardingAlgorithm> extendsShardingAlgorithm{ /** *@param所有可能的分片表(或分片庫(kù))名稱 *@param分片鍵的值 *@return根據(jù)分片鍵的值,找到對(duì)應(yīng)的分片表(或分片庫(kù))名稱并返回 */ StringdoSharding( CollectionavailableTargetNames, PreciseShardingValueshardingValue ); }packageorg.apache.shardingsphere.api.sharding.standard; ...... publicinterfaceRangeShardingAlgorithm> extendsShardingAlgorithm{ /** *@param所有可能的分片表(或分片庫(kù))名稱 *@param分片鍵的值 *@return根據(jù)分片鍵的值,找到對(duì)應(yīng)的分片表(或分片庫(kù))名稱并返回 */ CollectiondoSharding( CollectionavailableTargetNames, RangeShardingValueshardingValue ); }

標(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è)例子幫助你理解以上兩段話的含義。以 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%6in(0,2,4)的查詢分片到t_order1 t_order.order_id%6in(1,3,5)的查詢分片到t_order1 策略三:經(jīng)過(guò)估算訂單不超過(guò) 60000個(gè),設(shè)置 6 個(gè)分片 t_order.order_idbetween0and10000的查詢分片到t_order0 t_order.order_idbetween10000and20000的查詢分片到t_order1 t_order.order_idbetween20000and30000的查詢分片到t_order2 t_order.order_idbetween30000and40000的查詢分片到t_order3 t_order.order_idbetween40000and50000的查詢分片到t_order4 t_order.order_idbetween50000and60000的查詢分片到t_order5 策略四:經(jīng)過(guò)估算訂單不超過(guò) 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 表

寫(xiě)到分片配置里(無(wú)論是通過(guò)配置 API 還是通過(guò)配置文件),那我就能知道如何去路由 SQL,即根據(jù)分片鍵的值,找到對(duì)應(yīng)的分片表(或分片庫(kù))。

有了這些配置,我就能幫你們透明處理如下 SQL 語(yǔ)句,不管實(shí)際的物理分片是怎樣的:

--注:使用 t_order.order_id 作為 t_order 表的分片鍵 SELECTo.*FROMt_orderoWHEREo.order_id=10; SELECTo.*FROMt_orderoWHEREo.order_idIN(10,11); SELECTo.*FROMt_orderoWHEREo.order_id>10; SELECTo.*FROMt_orderoWHEREo.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; DELETEFROMt_orderoWHEREo.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。

packageorg.apache.shardingsphere.core.strategy.route.complex; ...... publicfinalclassComplexShardingStrategyimplementsShardingStrategy{ @Getter privatefinalCollectionshardingColumns; /** *要配合ComplexKeysShardingAlgorithm使用復(fù)合分片策略 */ privatefinalComplexKeysShardingAlgorithmshardingAlgorithm; publicComplexShardingStrategy( //傳入分片配置 finalComplexShardingStrategyConfigurationcomplexShardingStrategyConfig ){ ...... //從配置中提取分片鍵 shardingColumns=newTreeSet<>(String.CASE_INSENSITIVE_ORDER); shardingColumns.addAll( Splitter .on(",") .trimResults() .splitToList(complexShardingStrategyConfig.getShardingColumns()) ); //從配置中提取分片算法 shardingAlgorithm=complexShardingStrategyConfig.getShardingAlgorithm(); } @SuppressWarnings("unchecked") @Override publicCollectiondoSharding( //所有可能的分片表(或分片庫(kù))名稱 finalCollectionavailableTargetNames, //分片鍵的值 finalCollectionshardingValues, finalConfigurationPropertiesproperties ){ Map>>columnShardingValues =newHashMap<>(shardingValues.size(),1); Map>>columnRangeValues =newHashMap<>(shardingValues.size(),1); StringlogicTableName=""; //提取多個(gè)分片鍵的值 for(RouteValueeach:shardingValues){ if(eachinstanceofListRouteValue){ columnShardingValues.put( each.getColumnName(), ((ListRouteValue)each).getValues() ); }elseif(eachinstanceofRangeRouteValue){ columnRangeValues.put( each.getColumnName(), ((RangeRouteValue)each).getValueRange() ); } logicTableName=each.getTableName(); } CollectionshardingResult //調(diào)用shardingAlgorithm.doSharding()根據(jù)分片鍵的值找到對(duì)應(yīng)的 //分片表(或分片庫(kù))名稱并返回,shardingAlgorithm.doSharding() //由你們自己實(shí)現(xiàn) =shardingAlgorithm.doSharding( availableTargetNames, newComplexKeysShardingValue( logicTableName, columnShardingValues, columnRangeValues) ); Collectionresult=newTreeSet<>(String.CASE_INSENSITIVE_ORDER); result.addAll(shardingResult); //根據(jù)分片鍵的值,找到對(duì)應(yīng)的分片表(或分片庫(kù))名稱并返回 returnresult; } }

其中 ComplexKeysShardingAlgorithm(接口)的源代碼為:

packageorg.apache.shardingsphere.api.sharding.complex; ...... publicinterfaceComplexKeysShardingAlgorithm> extendsShardingAlgorithm{ /** *@param所有可能的分片表(或分片庫(kù))名稱 *@param分片鍵的值 *@return根據(jù)分片鍵的值,找到對(duì)應(yīng)的分片表(或分片庫(kù))名稱并返回 */ CollectiondoSharding( CollectionavailableTargetNames, ComplexKeysShardingValueshardingValue ); }

復(fù)合分片策略提供對(duì) SQL 語(yǔ)句中的操作符 =、>、<、>=、<=、IN 和 ETWEEN AND 的分片操作支持。

復(fù)合分片策略支持多分片鍵,例如對(duì) 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)過(guò)估算訂單不超過(guò) 60000個(gè)、用戶不超過(guò) 1000個(gè),設(shè)置 4 個(gè)分片 t_order.order_idbetween0and40000&&t_order.user_idbetween0and500的查詢分片到t_order0 t_order.order_idbetween0and40000&&t_order.user_idbetween500and1000的查詢分片到t_order1 t_order.order_idbetween40000and60000&&t_order.user_idbetween0and500的查詢分片到t_order2 t_order.order_idbetween40000and60000&&t_order.user_idbetween500and1000的查詢分片到t_order3 ......

那你就可以把以下三項(xiàng):

1)分片鍵 order_id 和 user_id

2)描述以上分片策略內(nèi)容的 ComplexKeysShardingAlgorithm(接口)的實(shí)現(xiàn)類

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 表的分片鍵 SELECTo.*FROMt_orderoWHEREo.order_id=10; SELECTo.*FROMt_orderoWHEREo.order_idIN(10,11); SELECTo.*FROMt_orderoWHEREo.order_id>10; SELECTo.*FROMt_orderoWHEREo.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; DELETEFROMt_orderoWHEREo.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?>10ANDuser_id>1000; SELECTo.*FROMt_orderoWHEREo.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?>10ANDuser_id>1000; DELETEFROMt_orderoWHEREo.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)分片策略和符合分片策略的代碼類似,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 查詢到具體的分片了。就像下面這樣:

Stringsql="SELECT*FROMt_order"; try( //HintManager是使用“暗示”的工具,它會(huì)把暗示的分片值放入 //當(dāng)前線程上下文(ThreadLocal)中,這樣當(dāng)前線程執(zhí)行SQL的 //時(shí)候就能獲取到分片值 HintManagerhintManager=HintManager.getInstance(); Connectionconn=dataSource.getConnection(); PreparedStatementpstmt=conn.prepareStatement(sql); ){ hintManager.setDatabaseShardingValue(3); try(ResultSetrs=pstmt.executeQuery()){ //若t_order僅僅使用order_id作為分片鍵,則這里根據(jù)暗 //示獲取了分片值,因此上面的 SQL 的實(shí)際執(zhí)行效果相當(dāng)于: //SELECT*FROMt_orderwhereorder_id=3 while(rs.next()){ //... } } }

四、不分片策略

對(duì)應(yīng) NoneShardingStrategy,這是一個(gè) final class。由于我并不要求所有的表(或庫(kù))都進(jìn)行水平分片,因此當(dāng)工程師要通過(guò)我執(zhí)行對(duì)不分片表(或庫(kù))的 SQL 查詢時(shí),就要使用這個(gè)不分片策略。NoneShardingStrategy 的源碼為:

packageorg.apache.shardingsphere.core.strategy.route.none; ...... @Getter publicfinalclassNoneShardingStrategyimplementsShardingStrategy{ privatefinalCollectionshardingColumns=Collections.emptyList(); @Override publicCollectiondoSharding( //所有可能的分片表(或分片庫(kù))名稱 finalCollectionavailableTargetNames, //分片鍵的值 finalCollectionshardingValues, finalConfigurationPropertiesproperties ){ //不需要任何算法,不進(jìn)行任何邏輯處理,直接返回 //所有可能的分片表(或分片庫(kù))名稱 returnavailableTargetNames; } }

五、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)查詢不會(huì)出現(xiàn)笛卡爾積關(guān)聯(lián),關(guān)聯(lián)查詢效率將大大提升。舉例說(shuō)明,如果 SQL 為:

SELECTi.*FROMt_orderoJOINt_order_itemiONo.order_id=i.order_idWHEREo.order_idIN(10,11);

在不配置綁定表關(guān)系時(shí),假設(shè)分片鍵 order_id 將數(shù)值 10 路由至第 0 片,將數(shù)值 11 路由至第 1 片,那么路由后的 SQL 應(yīng)該為 4 條,它們呈現(xiàn)為笛卡爾積,這種情況是我最不愿意處理的,我要考慮所有可能的分組合,它的工作量實(shí)在太大了:

SELECTi.*FROMt_order0oJOINt_order_item0iONo.order_id=i.order_idWHEREo.order_idIN(10,11); SELECTi.*FROMt_order0oJOINt_order_item1iONo.order_id=i.order_idWHEREo.order_idIN(10,11); SELECTi.*FROMt_order1oJOINt_order_item0iONo.order_id=i.order_idWHEREo.order_idIN(10,11); SELECTi.*FROMt_order1oJOINt_order_item1iONo.order_id=i.order_idWHEREo.order_idIN(10,11);

而在配置綁定表關(guān)系后,路由的 SQL 只有 2 條:

SELECTi.*FROMt_order0oJOINt_order_item0iONo.order_id=i.order_idWHEREo.order_idIN(10,11); SELECTi.*FROMt_order1oJOINt_order_item1iONo.order_id=i.order_idWHEREo.order_idIN(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è)主要流程。

039

當(dāng)一個(gè)應(yīng)用服務(wù)器節(jié)點(diǎn)將一個(gè)面向邏輯表編寫(xiě)的查詢 SQL 交給我之后,我要做下面幾件事:

1)SQL 解析(由我內(nèi)核中的解析引擎完成):先通過(guò)詞法解析器將邏輯 SQL 拆分為一個(gè)個(gè)不可再分的單詞,再使用語(yǔ)法解析器對(duì) SQL 進(jìn)行理解,并最終提煉出解析上下文。

2)SQL 路由(由我內(nèi)核中的路由引擎完成):根據(jù)解析上下文匹配用戶配置的分片策略(關(guān)于用戶配置的分片策略,我后文會(huì)慢慢解釋),并生成路由路徑,路由路徑指示了 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ò)多線程執(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è)信息、查詢條件以及可能需要修改的占位符的標(biāo)記。簡(jiǎn)單來(lái)說(shuō)就是我要理解這句 SQL,明白它的結(jié)構(gòu)和意圖。所幸,SQL 是一個(gè)語(yǔ)法簡(jiǎn)單的語(yǔ)言,SQL 解析這件事情并不復(fù)雜。

我先使用解析引擎的詞法解析器用于將 SQL 拆解為不可再分的原子符號(hào),我把它們叫做 Token,并將其歸類為關(guān)鍵字、表達(dá)式、字面量、操作符,再使用解析引擎的語(yǔ)法解析器將 SQL 轉(zhuǎn)換為抽象語(yǔ)法樹(shù)。

例如,以下 SQL:

SELECTid,nameFROMt_userWHEREstatus='ACTIVE'ANDage>18

被我的詞法解析器和語(yǔ)法解析器解析之后得到的抽象語(yǔ)法樹(shù)為:

040

在上圖中,為了便于理解,抽象語(yǔ)法樹(shù)中的關(guān)鍵字和操作符的 Token 用綠?表示,字面量的 Token 用紅?表示,灰?表示需要進(jìn)一步拆分。

最后,我通過(guò)對(duì)抽象語(yǔ)法樹(shù)的遍歷去提煉分片所需的上下文,并標(biāo)記有可能需要改寫(xiě)的位置。供分片使用的解析上下文包含查詢選擇項(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)查詢)。單片路由生成針對(duì)某個(gè)分片進(jìn)行查詢的 SQL,多片路由生成針對(duì)某些分片進(jìn)行查詢的 SQL。

不攜帶分片鍵的 SQL 則采用全路由(全路由是一種特殊的多片路由),即生成針對(duì)所有分片進(jì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*FROMt_userWHEREuser_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*FROMt_userWHEREuser_idin(1002,1003,1009)--路由到t_user2、t_user3、t_user4 SELECT*FROMt_userWHEREuser_id>1002ANDuser_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?

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è)修正、批量拆分。

041

一、標(biāo)識(shí)符改寫(xiě)

在水平分片的場(chǎng)景中,需要將 SQL 中的邏輯表名改寫(xiě)為路由之后所對(duì)應(yīng)的物理分片表名,索引名稱以及 Schema 名稱也要進(jìn)行邏輯名到物理名的改寫(xiě)。

1)表名稱改寫(xiě)

表名稱改寫(xiě)是指將找到邏輯表名在原始 SQL 中的位置,并將其改寫(xiě)為真實(shí)分片表名的過(guò)程。比如,若邏輯 SQL 為:

SELECTorder_idFROMt_orderWHEREorder_id=1;

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

SELECTorder_idFROMt_order1WHEREorder_id=1;

你或許會(huì)以為只要通過(guò)字符串查找和替換就可以達(dá)到 SQL 改寫(xiě)的效果,但事實(shí)并非如此,例如:

SELECTt_order.order_idFROMt_orderASt_orderWHEREt_order.order_id=1ANDremarks='備注t_orderxxx';

SQL 改寫(xiě)則僅需要改寫(xiě)表名稱就可以了,別名“t_order”、備注字段內(nèi)容“t_order”均無(wú)需改寫(xiě):

SELECTt_order.order_idFROMt_order_1ASt_orderWHEREt_order.order_id=1ANDremarks='備注t_orderxxx';

因此表名稱改寫(xiě)是一個(gè)典型的需要對(duì) SQL 進(jìn)行詞法和語(yǔ)法解析的場(chǎng)景,它依賴于 SQL 解析上下文,即依賴于對(duì) SQL 語(yǔ)義的理解,而不是簡(jiǎn)單的字符串替換!對(duì)于包含索引和 Schema 的 SQL 改寫(xiě)也是一樣。

2)索引名稱改寫(xiě)

索引名稱是另一個(gè)有可能改寫(xiě)的標(biāo)識(shí)符。在某些數(shù)據(jù)庫(kù)中(如 MySQL、SQLServer),索引是以表為維度創(chuàng)建的,在不同的表中的索引是可以重名的;而在另外的一些數(shù)據(jù)庫(kù)中(如 PostgreSQL、Oracle),索引是以數(shù)據(jù)庫(kù)為維度創(chuàng)建的,即使是作用在不同表上的索引,它們也要求其名稱的唯一性。這些瑣碎的規(guī)則都要納入我的索引改寫(xiě)算法的考量之中。

3)Schema 名稱改寫(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,我需要將用戶在 SQL 中書(shū)寫(xiě)的邏輯 Schema 改寫(xiě)為真實(shí)的數(shù)據(jù)庫(kù)分片 Schema。但我目前還不支持在 DQL(數(shù)據(jù)查詢語(yǔ)言,SELECT)和 DML(數(shù)據(jù)操縱語(yǔ)言,INSERT、UPDATE、DELETE 等)語(yǔ)句中使用 Schema,我只能改寫(xiě)數(shù)據(jù)庫(kù)管理語(yǔ)句中的 Schema,例如:

SHOWCOLUMNSFROMt_orderFROMorder_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ǔ)句,查詢邏輯表 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)題。

SELECTorder_id,user_idFROMt_orderORDERBYuser_id;

但如果 SQL 語(yǔ)句是:

SELECTorder_idFROMt_order0ORDERBYuser_id;

我的內(nèi)核在執(zhí)行階段就無(wú)法執(zhí)行,因?yàn)檫@個(gè)語(yǔ)句查詢的結(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é)果為:

SELECTorder_id,user_idFROMt_order0ORDERBYuser_id;

再比如:

--補(bǔ)列前(結(jié)果集o.*中不包含排序鍵order_item_id) SELECTo.*FROMt_ordero,t_order_itemiWHEREo.order_id=i.order_idORDERBYuser_id,order_item_id; --補(bǔ)列后(結(jié)果集o.*中包含排序鍵order_item_id) SELECTo.*,order_item_idFROMt_ordero,t_order_itemiWHEREo.order_id=i.order_idORDERBYuser_id,order_item_id;

2)分組補(bǔ)列

和排序補(bǔ)列類似,分組補(bǔ)列的目的是在結(jié)果字段中補(bǔ)全分組鍵,比如:

--補(bǔ)列前(結(jié)果集order_id中不包含分組鍵user_id) SELECTorder_idFROMt_orderGROUPBYuser_id --補(bǔ)列后(結(jié)果集order_id中包含分組鍵user_id) SELECTorder_id,user_idFROMt_orderGROUPBYuser_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:

SELECTAVG(age)FROMt_userWHEREage>=18;

會(huì)被補(bǔ)列處理成:

SELECTCOUNT(age)ASAVG_DERIVED_COUNT,SUM(age)ASAVG_DERIVED_SUMFROMt_userWHEREage>=18;

再經(jīng)過(guò)路由和改寫(xiě),最終執(zhí)行的 SQL 為:

SELECTCOUNT(age)ASAVG_DERIVED_COUNT,SUM(age)ASAVG_DERIVED_SUMFROMt_user0WHEREage>=18; SELECTCOUNT(age)ASAVG_DERIVED_COUNT,SUM(age)ASAVG_DERIVED_SUMFROMt_user1WHEREage>=18; SELECTCOUNT(age)ASAVG_DERIVED_COUNT,SUM(age)ASAVG_DERIVED_SUMFROMt_user2WHEREage>=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ú)法滿足數(shù)據(jù)表多分片場(chǎng)景下的主鍵的唯一性要求的,因此我提供了分布式自增主鍵的生成算法(如雪花算法),并且可以通過(guò)補(bǔ)列,讓使用方無(wú)需改動(dòng)現(xiàn)有代碼,即可將數(shù)據(jù)庫(kù)現(xiàn)有的自增主鍵透明地替換成分布式自增主鍵。舉例說(shuō)明,假設(shè)表 t_order 的主鍵是 order_id,原始的 SQL 為:

INSERTINTOt_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ě)為:

INSERTINTOt_example(id,`field1`,`field2`)VALUES(snow_flake_id,10,1);

上述 SQL 中的 snow_flake_id 表示自動(dòng)生成的分布式全局自增主鍵值。

顯然,所有的補(bǔ)列都是基于 SQL 語(yǔ)義進(jìn)行的,有賴于 SQL 的詞法和語(yǔ)法分析。因此,我還是要重復(fù)那句話:SQL 解析是 SQL 路由、改寫(xiě)、執(zhí)行和歸并的基礎(chǔ)。

三、分頁(yè)修正

從多個(gè)表分片中獲取分頁(yè)數(shù)據(jù)與單表的場(chǎng)景是不同的。假設(shè)每 10 條數(shù)據(jù)為一頁(yè),要從一個(gè)邏輯表中查詢 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è)查詢的 SQL 為:

SELECTageFROMt_userORDERBYageDESCLIMIT1,2;

若直接路由并改寫(xiě)成:

SELECTageFROMt_user0ORDERBYageDESCLIMIT1,2; SELECTageFROMt_user1ORDERBYageDESCLIMIT1,2;

得到的結(jié)果會(huì)出乎你的預(yù)料,下圖展示了不進(jìn)行 SQL 的改寫(xiě)的分頁(yè)執(zhí)行結(jié)果。

042

通過(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ù)。即:

SELECTageFROMt_userORDERBYageDESCLIMIT0,3;

路由并改寫(xiě)之后的結(jié)果為:

SELECTageFROMt_user0ORDERBYageDESCLIMIT0,3; SELECTageFROMt_user1ORDERBYageDESCLIMIT0,3;

下圖展示了進(jìn)行正確的 SQL 改寫(xiě)之后的分頁(yè)執(zhí)行結(jié)果:

043

在這種做法下,獲取數(shù)據(jù)的偏移量位置越靠后,使用 LIMIT 分頁(yè)方式的效率就越低。但有很多方法可以避免使用 LIMIT 進(jìn)行分頁(yè)。比如使用上次分頁(yè)數(shù)據(jù)結(jié)尾 ID 作為下次查詢條件的分頁(yè)方式等(我會(huì)在后文給出示例)。

四、批量拆分

1)批量插入拆分

在處理批量插入的 SQL 時(shí),如果插入的數(shù)據(jù)是跨分片的,那么需要對(duì) SQL 進(jìn)行改寫(xiě)來(lái)防止將多余的數(shù)據(jù)寫(xiě)入到數(shù)據(jù)庫(kù)中。舉例說(shuō)明,如下 SQL:

INSERTINTOt_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ě)入相同的記錄。雖然只有符合分片查詢條件的數(shù)據(jù)才能夠被查詢語(yǔ)句取出,但存在冗余數(shù)據(jù)的實(shí)現(xiàn)方案并不合理。因此我需要將路由后的 SQL 改寫(xiě)為:

INSERTINTOt_order0(order_id,xxx)VALUES(2,'xxx'); INSERTINTOt_order1(order_id,xxx)VALUES(1,'xxx'),(3,'xxx');

2)In 查詢拆分

使用 IN 的批量查詢與批量插入的情況相似,不過(guò)使用 IN 的批量查詢操作并不會(huì)導(dǎo)致數(shù)據(jù)查詢結(jié)果錯(cuò)誤(批量插入操作與批量查詢操作的不同之處在于,查詢語(yǔ)句中即使用了不存在于當(dāng)前分片的分片鍵值,也不會(huì)對(duì)結(jié)果產(chǎn)生影響。因此對(duì)批量查詢 SQL 進(jìn)行拆分并不是必須的,而插入操作則必須將多余的分片鍵值刪除)。

因此對(duì)于如以下 SQL 的批量拆分改寫(xiě),我偷了個(gè)懶:

SELECT*FROMt_orderWHEREorder_idIN(1,2,3);

直接路由并改寫(xiě)為:

SELECT*FROMt_order0WHEREorder_idIN(1,2,3); SELECT*FROMt_order1WHEREorder_idIN(1,2,3);

實(shí)際上,更好的改寫(xiě)結(jié)果是:

SELECT*FROMt_order0WHEREorder_idIN(2); SELECT*FROMt_order1WHEREorder_idIN(1,3);

這樣可以進(jìn)一步的提升查詢性能,但我的創(chuàng)造者給我設(shè)計(jì)的內(nèi)核并沒(méi)有進(jìn)行這種優(yōu)化。雖然 SQL 的執(zhí)行結(jié)果是正確的,但并未達(dá)到最優(yōu)的查詢效率。

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)求放入線程池去并發(fā)執(zhí)行,而是采用了復(fù)雜的控制策略。我的執(zhí)行引擎的工作目標(biāo)是平衡資源占用(資源包括數(shù)據(jù)庫(kù)連接、內(nèi)存和線程)與執(zhí)行效率(時(shí)間)。

在講解我的執(zhí)行引擎執(zhí)行 SQL 的過(guò)程之前,我要先向各位介紹我的執(zhí)行引擎的連接模式。

一個(gè)面向邏輯表編寫(xiě)的 SQL 交到我的手中,會(huì)被我路由、改寫(xiě)成面向多個(gè)物理分片表的 SQL(也可以稱為真實(shí) SQL)。執(zhí)行多個(gè)真實(shí) SQL,最理想的情況是為每個(gè)分片 SQL 查詢創(chuàng)建一個(gè)數(shù)據(jù)庫(kù)連接,且每個(gè)連接交由一個(gè)專門(mén)的線程來(lái)處理。但是計(jì)算機(jī)系統(tǒng)所能提供的資源是有限的,不可能讓進(jìn)程無(wú)限創(chuàng)建數(shù)據(jù)庫(kù)連接和線程。

從資源控制的角度看,業(yè)務(wù)方訪問(wèn)數(shù)據(jù)庫(kù)的連接數(shù)量應(yīng)當(dāng)有所限制(你們常用的數(shù)據(jù)庫(kù)連接池就在做這件事)。它能夠有效地防止某一業(yè)務(wù)操作過(guò)多地占用資源,從而將數(shù)據(jù)庫(kù)連接的資源耗盡,以致于影響其他業(yè)務(wù)的正常訪問(wèn)。特別是,在一個(gè)數(shù)據(jù)庫(kù)實(shí)例中存在較多分片表的情況下,一條不包含分片鍵的邏輯 SQL 經(jīng)過(guò)路由過(guò)程將產(chǎn)生大量落在同庫(kù)不同分片表的真實(shí) SQL,如果每條真實(shí) SQL 都占用一個(gè)獨(dú)立的連接,那么一次查詢無(wú)疑將會(huì)占用過(guò)多的資源。

從執(zhí)行效率的角度看,為每個(gè)分片查詢維持一個(gè)獨(dú)立的數(shù)據(jù)庫(kù)連接,可以更加有效的利用多線程來(lái)提升執(zhí)行效率,因?yàn)槿魹槊總€(gè)數(shù)據(jù)庫(kù)連接開(kāi)啟獨(dú)立的處理線程,可以并行處理查詢結(jié)果集。而且,為每個(gè)分片查詢維持一個(gè)獨(dú)立的數(shù)據(jù)庫(kù)連接,還能夠避免過(guò)早的將查詢結(jié)果集加載至數(shù)據(jù)庫(kù)客戶端(我,Sharding-JDBC,數(shù)據(jù)庫(kù)中間件,運(yùn)行在應(yīng)用程序所在的 JVM 上,就是一個(gè)數(shù)據(jù)庫(kù)客戶端)的內(nèi)存,代以流式處理方式來(lái)處理。若為每個(gè)分片查詢維持一個(gè)獨(dú)立的數(shù)據(jù)庫(kù)連接,能夠持有查詢結(jié)果集游標(biāo)位置的引用,在需要獲取相應(yīng)數(shù)據(jù)時(shí)移動(dòng)游標(biāo)即可。以結(jié)果集游標(biāo)下移進(jìn)行結(jié)果歸并的方式,稱之為流式歸并,它無(wú)需將結(jié)果數(shù)據(jù)全數(shù)加載至數(shù)據(jù)庫(kù)客戶端內(nèi)存,可以有效的節(jié)省數(shù)據(jù)庫(kù)客戶端內(nèi)存資源,進(jìn)而減少數(shù)據(jù)庫(kù)客戶端垃圾回收的頻次(說(shuō)的簡(jiǎn)單些,即先將查詢結(jié)果集保留在數(shù)據(jù)庫(kù)服務(wù)器的緩沖區(qū)內(nèi),然后客戶端這邊采用流式處理方式一點(diǎn)點(diǎn)獲取數(shù)據(jù)來(lái)處理。避免一次性將結(jié)果集送到客戶端,占用客戶端太多內(nèi)存)。當(dāng)無(wú)法保證每個(gè)分片查詢持有一個(gè)獨(dú)立數(shù)據(jù)庫(kù)連接時(shí),則需要在復(fù)用該數(shù)據(jù)庫(kù)連接獲取下一個(gè)分片查詢的結(jié)果集之前,將當(dāng)前的分片查詢結(jié)果集全數(shù)加載至內(nèi)存。因此,即使可以采用流式歸并,在此場(chǎng)景下也將退化為內(nèi)存歸并。

綜上所述,我的執(zhí)行引擎一方面想控制數(shù)據(jù)庫(kù)連接的數(shù)量;另一方面想為每個(gè)分片查詢維持一個(gè)獨(dú)立的數(shù)據(jù)庫(kù)連接,以采用更優(yōu)的流式歸并模式達(dá)到對(duì)數(shù)據(jù)庫(kù)客戶端內(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)這兩種類型。內(nèi)存限制模式要求更多的連接,但占用更少的客戶端內(nèi)存;而連接限制模式要求更少的連接,但占用更多的客戶端內(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ò)多線程的方式并發(fā)處理,以達(dá)成執(zhí)行效率最大化。并且在 SQL 滿足條件情況下,優(yōu)先選擇流式歸并,以防止數(shù)據(jù)庫(kù)客戶端出現(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ù),仍然采用多線程處理對(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ù)連接,以保證在線系統(tǒng)數(shù)據(jù)庫(kù)資源能夠被更多的應(yīng)用所使用。

我最初想將使用何種模式的決定權(quán)交由你們這些工程師來(lái)配置,讓你們依據(jù)自己業(yè)務(wù)的實(shí)際場(chǎng)景需求選擇使用內(nèi)存限制模式或連接限制模式。這種想法將兩難的選擇的決定權(quán)交由用戶,使得用戶必須要了解這兩種模式的利弊,并依據(jù)業(yè)務(wù)場(chǎng)景需求進(jìn)行選擇。而且這種靜態(tài)的連接模式配置,缺乏靈活性。

在實(shí)際的使用場(chǎng)景中,面對(duì)不同的邏輯 SQL,每次的路由結(jié)果是不同的。這就意味著某些操作可能需要使用內(nèi)存歸并,而某些操作則可能選擇流式歸并更優(yōu),具體采用哪種方式不應(yīng)該由用戶在我啟動(dòng)之前配置好,而是應(yīng)該根據(jù)具體的邏輯 SQL,來(lái)動(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)前場(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í)行引擎,用戶只需配置 maxConnectionSizePerQuery 即可,該參數(shù)表示進(jì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è)連接要處理的查詢請(qǐng)求/要執(zhí)行的 SQL。結(jié)果集分組具體步驟如下:

1)先將 SQL 的路由結(jié)果按照數(shù)據(jù)源的名稱進(jìn)行分組;

2)然后通過(guò)下圖的公式,可以獲得每個(gè)數(shù)據(jù)庫(kù)實(shí)例在 maxConnectionSizePerQuery 的允許范圍內(nèi),每個(gè)連接需要執(zhí)行的 SQL 路由結(jié)果組,并計(jì)算出本次請(qǐng)求的最優(yōu)連接模式。

044

在 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ō),在同一次查詢中,如果該查詢被路由至一個(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è)一次查詢需要在某一數(shù)據(jù)源上獲取兩個(gè)數(shù)據(jù)庫(kù)連接,并路由至同一個(gè)數(shù)據(jù)庫(kù)的兩個(gè)分表查詢。則有可能出現(xiàn)查詢 A 已獲取到該數(shù)據(jù)源的 1 個(gè)數(shù)據(jù)庫(kù)連接,并等待獲取另一個(gè)數(shù)據(jù)庫(kù)連接;而查詢 B 也已經(jīng)在該數(shù)據(jù)源上獲取到的一個(gè)數(shù)據(jù)庫(kù)連接,并同樣等待另一個(gè)數(shù)據(jù)庫(kù)連接的獲取。如果數(shù)據(jù)庫(kù)連接池的允許最大連接數(shù)是 2,那么這 2 個(gè)查詢請(qǐng)求將永久的等待下去。下圖描繪了死鎖的情況。

045

我為了避免死鎖的出現(xiàn),在獲取數(shù)據(jù)庫(kù)連接時(shí)進(jìn)行了同步處理。具體來(lái)說(shuō)就是在創(chuàng)建執(zhí)行單元時(shí),以原子性的方式一次性獲取本次 SQL 請(qǐng)求所需的全部數(shù)據(jù)庫(kù)連接,杜絕了每次查詢請(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í),所有的查詢結(jié)果集將在裝載至內(nèi)存之后釋放掉數(shù)據(jù)庫(kù)連接資源,因此不會(huì)產(chǎn)生死鎖等待的問(wèn)題。

二、執(zhí)行階段

該階段用于真正地執(zhí)行 SQL,它分為分組執(zhí)行和查詢結(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)存查詢結(jié)果集或流式查詢結(jié)果集,并將其傳遞至結(jié)果歸并引擎,以進(jìn)行下一步的?作。

我的執(zhí)行引擎的整體工作流如下圖所示。

046

2)歸并結(jié)果集:請(qǐng)看下一節(jié)。

4.3.5. 結(jié)果歸并

我建議你好好看看這一節(jié),它里面包含了很多數(shù)據(jù)結(jié)構(gòu)的知識(shí)。

我將從各個(gè)數(shù)據(jù)分片上獲取的結(jié)果集,組合成為一個(gè)總的結(jié)果集并正確的返回至請(qǐng)求客戶端,這個(gè)過(guò)程就是結(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)存中,再通過(guò)統(tǒng)一的分組、排序以及聚合等計(jì)算之后,再將其封裝成為能被請(qǐng)求客戶端逐條訪問(wèn)的歸并結(jié)果集返回。

3)裝飾者歸并

裝飾者歸并是指對(duì)常規(guī)的結(jié)果集歸并利用裝飾者模式進(jìn)行功能增強(qiáng),目前裝飾者歸并有分頁(yè)裝飾歸并和聚合裝飾歸并這 2 種類型。我在前文講過(guò),包含聚合函數(shù)的 SQL 經(jīng)過(guò)改寫(xiě)之后要在歸并階段重新計(jì)算聚合,這就是裝飾者歸并要做的事情;同樣,包含分頁(yè)信息的 SQL 經(jīng)過(guò)改寫(xiě)之后要在歸并階段重新進(jìn)行分頁(yè)計(jì)算,這也是裝飾者歸并要做的事情。

我支持的結(jié)果歸并從功能上分為遍歷、排序、分組、分頁(yè)和聚合 5 種類型:

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)查詢的邏輯 SQL 為:

SELECTageFROMt_userwhereage

它被路由和改寫(xiě)之后的結(jié)果為:

SELECTageFROMt_user0whereage

顯然它最終產(chǎn)生三個(gè)分片結(jié)果集,對(duì)這三個(gè)結(jié)果集進(jìn)行歸并,只需將他們串聯(lián)成鏈表返回給請(qǐng)求客戶端即可。請(qǐng)求客戶端讀取總的歸并結(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)查詢的邏輯 SQL 為:

SELECTageFROMt_userorderbyageDESC

它被路由和改寫(xiě)之后的結(jié)果為:

SELECTageFROMt_user0orderbyageDESC SELECTageFROMt_user1orderbyageDESC SELECTageFROMt_user2orderbyageDESC

由于在 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ǔ)句的分片查詢結(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ì)列。

047

下圖則展現(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)返回至查詢客戶端,并且將游標(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ì)列首位。

048

在進(jìn)行第二次 next 時(shí),只需要將目前排列在隊(duì)列首位的 t_user2 彈出隊(duì)列,并且將其數(shù)據(jù)結(jié)果集游標(biāo)指向的值返回至客戶端,并下移游標(biāo),繼續(xù)加入隊(duì)列排隊(duì),以此類推。當(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 獲取每位考生的總分:

SELECTname,SUM(score)assum_scoreFROMt_scoreGROUPBYnameORDERBYnameasc;

以上 SQL 被路由和改寫(xiě)之后的結(jié)果為:

SELECTname,SUM(score)assum_scoreFROMt_score_javaGROUPBYnameORDERBYnameasc; SELECTname,SUM(score)assum_scoreFROMt_score_goGROUPBYnameORDERBYnameasc; SELECTname,SUM(score)assum_scoreFROMt_score_pythonGROUPBYnameORDERBYnameasc;

在分組項(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ù)值中,因此可以采用流式歸并。如下圖所示:

049

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

050

通過(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ù)從高至低排序:

SELECTname,SUM(score)assum_scoreFROMt_scoreGROUPBYnameORDERBYscoreDESC;

那么各個(gè)分片結(jié)果集中的數(shù)據(jù)如下圖所示,顯然是無(wú)法像上圖那樣進(jìn)行流式歸并的,不信你按照上一張圖的過(guò)程動(dòng)筆畫(huà)一下試試 :-)

051

當(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 種類型。

比較類型的聚合函數(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ù)字段)。現(xiàn)在要通過(guò) SQL 獲取每位考生的單科最高分:

SELECTname,MAX(score)FROMt_scoreGROUPBYname;

以上 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)化為流式分組歸并方式 SELECTname,MAX(score)asmax_scoreFROMt_score_javaGROUPBYnameORDERBYnameASC; SELECTname,MAX(score)asmax_scoreFROMt_score_goGROUPBYnameORDERBYnameASC; SELECTname,MAX(score)asmax_scoreFROMt_score_pythonGROUPBYnameORDERBYnameASC;

052

通過(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ò)程屬于流式歸并。

053

以上是 MAX 函數(shù)的聚合方式,MIN 函數(shù)的聚合方式類似,不再贅述。

累加類型的聚合函數(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è)歸并

上文所述的所有歸并類型都可能進(jìn)行分頁(yè)。分頁(yè)也是追加在其他歸并類型之上的裝飾過(guò)程,我的創(chuàng)造者通過(guò)裝飾者模式賦予我對(duì)數(shù)據(jù)結(jié)果集的分頁(yè)能力。若邏輯 SQL 要查詢第 M 頁(yè)的數(shù)據(jù),查詢結(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è)功能比較容易讓使用者誤解,用戶通常認(rèn)為分頁(yè)歸并會(huì)占用大量?jī)?nèi)存。用戶非常容易產(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ò)索引查詢數(shù)據(jù),因此如果可以保證 ID 的連續(xù)性,通過(guò) ID 進(jìn)行分頁(yè)是比較好的解決方案,例如:

SELECT*FROMt_orderWHEREid>100000ANDid<=?100010?ORDER?BY?id;

或通過(guò)記錄上次查詢結(jié)果的最后一條記錄的 ID 進(jìn)行下一頁(yè)的查詢,例如:

SELECT*FROMt_orderWHEREid>10000000LIMIT10;

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ù))名稱 *@param分片鍵的值 *@return根據(jù)分片鍵的值,找到對(duì)應(yīng)的分片表(或分片庫(kù))名稱并返回 */ CollectiondoSharding( CollectionavailableTargetNames, ComplexKeysShardingValueshardingValue );

我并非法力無(wú)邊,我還有很多局限。在單片路由和多片路由的場(chǎng)景下,我全面支持 DML、DDL、DCL、TCL 和部分 DAL,支持分頁(yè)、去重、排序、分組、聚合、不跨數(shù)據(jù)庫(kù)的關(guān)聯(lián)查詢等操作。但在多片路由的場(chǎng)景下,我不支持 HAVING、UNION 等操作,對(duì)子查詢的支持也有限。其他種種細(xì)節(jié),一篇文章,難以詳述。

我是 Sharding-JDBC,關(guān)于我的基本用法和基本原理,我說(shuō)完了,你禿了嗎?

責(zé)任編輯:xj

原文標(biāo)題:分庫(kù)分表之 Sharding-JDBC 中間件,看這篇真的夠了!

文章出處:【微信公眾號(hào):數(shù)據(jù)分析與開(kāi)發(fā)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

聲明:本文內(nèi)容及配圖由入駐作者撰寫(xiě)或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問(wèn)題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • 中間件
    +關(guān)注

    關(guān)注

    0

    文章

    65

    瀏覽量

    18213
  • JDBC
    +關(guān)注

    關(guān)注

    0

    文章

    25

    瀏覽量

    13418
  • sharding
    +關(guān)注

    關(guān)注

    0

    文章

    5

    瀏覽量

    7926

原文標(biāo)題:分庫(kù)分表之 Sharding-JDBC 中間件,看這篇真的夠了!

文章出處:【微信號(hào):DBDevs,微信公眾號(hào):數(shù)據(jù)分析與開(kāi)發(fā)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

收藏 人收藏

    評(píng)論

    相關(guān)推薦

    AB伺服軟件使用方法

    AB伺服軟件使用方法
    發(fā)表于 12-24 14:45 ?0次下載

    示波器高壓探頭使用方法

    示波器高壓探頭是一種用于測(cè)量高電壓信號(hào)的儀器,通常用于高壓電源、電機(jī)、變壓器等電氣設(shè)備的測(cè)試。以下是示波器高壓探頭的詳細(xì)使用方法
    的頭像 發(fā)表于 10-03 16:43 ?532次閱讀

    探針頭型使用方法有哪些

    探針頭型的使用方法多種多樣,具體取決于探針頭型的類型、被測(cè)對(duì)象的特性以及測(cè)試需求。以下是一些常見(jiàn)探針頭型的使用方法概述: 1. 凹頭探針 用途 :主要用于測(cè)試長(zhǎng)導(dǎo)腳、端子及繞線柱等較長(zhǎng)或較粗的被測(cè)點(diǎn)
    的頭像 發(fā)表于 09-07 10:57 ?862次閱讀

    MSP430 FRAM技術(shù)–使用方法和最佳實(shí)踐

    電子發(fā)燒友網(wǎng)站提供《MSP430 FRAM技術(shù)–使用方法和最佳實(shí)踐.pdf》資料免費(fèi)下載
    發(fā)表于 08-23 09:23 ?0次下載
    MSP430 FRAM技術(shù)–<b class='flag-5'>使用方法</b>和最佳實(shí)踐

    DC/DC模擬的基本使用方法和特性確認(rèn)方法

    本篇介紹了DC/DC模擬的基本使用方法及確認(rèn)基本特性的方法
    的頭像 發(fā)表于 08-20 17:08 ?790次閱讀
    DC/DC模擬的基本<b class='flag-5'>使用方法</b>和特性確認(rèn)<b class='flag-5'>方法</b>

    DC/DC模擬器的特征和使用方法

    本篇介紹了特瑞仕在官網(wǎng)提供的DC/DC模擬器的特征和使用方法。
    的頭像 發(fā)表于 07-18 16:17 ?804次閱讀
    DC/DC模擬器的特征和<b class='flag-5'>使用方法</b>

    淺談錫膏的儲(chǔ)存及使用方法

    錫膏(焊錫膏)是電子組裝過(guò)程中常用的材料,它的儲(chǔ)存和使用方法對(duì)保證焊接質(zhì)量和性能至關(guān)重要。以下是詳細(xì)的儲(chǔ)存及使用方法
    的頭像 發(fā)表于 06-27 10:02 ?1035次閱讀

    可編程電源使用方法

    可編程電源使用方法 可編程電源使用方法 摘要:本文詳細(xì)介紹了可編程電源的使用方法,包括其基本概念、主要功能、選擇原則、操作步驟、注意事項(xiàng)以及實(shí)際應(yīng)用案例,旨在幫助讀者全面了解可編程電源
    的頭像 發(fā)表于 06-10 15:29 ?1136次閱讀

    現(xiàn)場(chǎng)總線的使用方法與注意事項(xiàng)

    的穩(wěn)定可靠運(yùn)行,正確的使用方法和注意事項(xiàng)至關(guān)重要。本文將詳細(xì)介紹現(xiàn)場(chǎng)總線的使用方法和注意事項(xiàng),以供讀者參考。
    的頭像 發(fā)表于 06-06 11:49 ?862次閱讀

    手柄控制代碼及使用方法

    手柄控制代碼及使用方法
    的頭像 發(fā)表于 05-15 10:19 ?2046次閱讀

    鉗形電流表的使用方法和注意事項(xiàng)

    鉗形電流表,作為電氣測(cè)量和測(cè)試的重要工具,其使用方法和注意事項(xiàng)對(duì)于保障測(cè)量結(jié)果的準(zhǔn)確性和操作人員的安全至關(guān)重要。本文將從鉗形電流表的使用方法和注意事項(xiàng)兩個(gè)方面進(jìn)行詳細(xì)闡述,以期為讀者提供一個(gè)全面而深入的了解。
    的頭像 發(fā)表于 05-14 16:14 ?3711次閱讀

    光纖熱縮管正確使用方法

    光纖熱縮管是一種用于保護(hù)光纖連接點(diǎn)的材料,其主要功能是在光纖連接點(diǎn)處提供保護(hù),防止光纖受到機(jī)械傷害或環(huán)境因素的影響。在正確使用光纖熱縮管之前,首先需要了解其正確的使用方法。本文將詳細(xì)介紹光纖熱縮管
    的頭像 發(fā)表于 04-07 17:13 ?1656次閱讀

    手機(jī)信號(hào)屏蔽器:功能、原理及使用方法詳解

    深圳特信電子|手機(jī)信號(hào)屏蔽器:功能、原理及使用方法詳解
    的頭像 發(fā)表于 04-01 09:09 ?3427次閱讀

    555集成芯片的使用方法

    555集成芯片的使用方法主要依賴于其特定的引腳功能和電路設(shè)計(jì)。
    的頭像 發(fā)表于 03-25 14:39 ?1602次閱讀

    集成芯片的使用方法

    需要注意的是,不同類型的集成芯片具有不同的使用方法和功能,因此在實(shí)際應(yīng)用中,需要仔細(xì)閱讀芯片的數(shù)據(jù)手冊(cè)和相關(guān)文檔,了解其具體的使用要求和步驟。此外,對(duì)于復(fù)雜的電路設(shè)計(jì)和系統(tǒng)應(yīng)用,可能需要具備相應(yīng)的電子技術(shù)和專業(yè)知識(shí)。
    的頭像 發(fā)表于 03-19 15:59 ?1568次閱讀