今天我們介紹一下 Sharding-JDBC
框架和快速的搭建一個(gè)分庫分表案例,為講解后續(xù)功能點(diǎn)準(zhǔn)備好環(huán)境甫题。
一馁筐、Sharding-JDBC 簡(jiǎn)介
Sharding-JDBC
最早是當(dāng)當(dāng)網(wǎng)內(nèi)部使用的一款分庫分表框架,到2017年的時(shí)候才開始對(duì)外開源坠非,這幾年在大量社區(qū)貢獻(xiàn)者的不斷迭代下敏沉,功能也逐漸完善,現(xiàn)已更名為 ShardingSphere
炎码,2020年4?16?正式成為 Apache
軟件基?會(huì)的頂級(jí)項(xiàng)?盟迟。
隨著版本的不斷更迭 ShardingSphere
的核心功能也變得多元化起來。從最開始 Sharding-JDBC 1.0 版本只有數(shù)據(jù)分片潦闲,到 Sharding-JDBC 2.0 版本開始支持?jǐn)?shù)據(jù)庫治理(注冊(cè)中心攒菠、配置中心等等),再到 Sharding-JDBC 3.0版本又加分布式事務(wù) (支持 Atomikos
歉闰、Narayana
辖众、Bitronix
、Seata
)和敬,如今已經(jīng)迭代到了 Sharding-JDBC 4.0 版本。
現(xiàn)在的 ShardingSphere 不單單是指某個(gè)框架而是一個(gè)生態(tài)圈,這個(gè)生態(tài)圈 Sharding-JDBC
罪针、Sharding-Proxy
和 Sharding-Sidecar
這三款開源的分布式數(shù)據(jù)庫中間件解決方案所構(gòu)成普办。
ShardingSphere
的前身就是 Sharding-JDBC
,所以它是整個(gè)框架中最為經(jīng)典舱痘、成熟的組件变骡,我們先從 Sharding-JDBC
框架入手學(xué)習(xí)分庫分表。
二衰粹、核心概念
在開始 Sharding-JDBC
分庫分表具體實(shí)戰(zhàn)之前锣光,我們有必要先了解分庫分表的一些核心概念。
分片
一般我們?cè)谔岬椒謳旆直淼臅r(shí)候铝耻,大多是以水平切分模式(水平分庫誊爹、分表)為基礎(chǔ)來說的蹬刷,數(shù)據(jù)分片將原本一張數(shù)據(jù)量較大的表 t_order
拆分生成數(shù)個(gè)表結(jié)構(gòu)完全一致的小數(shù)據(jù)量表 t_order_0
、t_order_1
频丘、···办成、t_order_n
,每張表只存儲(chǔ)原大表中的一部分?jǐn)?shù)據(jù)搂漠,當(dāng)執(zhí)行一條SQL
時(shí)會(huì)通過 分庫策略
迂卢、分片策略
將數(shù)據(jù)分散到不同的數(shù)據(jù)庫、表內(nèi)桐汤。
數(shù)據(jù)節(jié)點(diǎn)
數(shù)據(jù)節(jié)點(diǎn)是分庫分表中一個(gè)不可再分的最小數(shù)據(jù)單元(表)而克,它由數(shù)據(jù)源名稱和數(shù)據(jù)表組成,例如上圖中 order_db_1.t_order_0
怔毛、order_db_2.t_order_1
就表示一個(gè)數(shù)據(jù)節(jié)點(diǎn)员萍。
邏輯表
邏輯表是指一組具有相同邏輯和數(shù)據(jù)結(jié)構(gòu)表的總稱。比如我們將訂單表t_order
拆分成 t_order_0
··· t_order_9
等 10張表拣度。此時(shí)我們會(huì)發(fā)現(xiàn)分庫分表以后數(shù)據(jù)庫中已不在有 t_order
這張表碎绎,取而代之的是 t_order_n
,但我們?cè)诖a中寫 SQL
依然按 t_order
來寫抗果。此時(shí) t_order
就是這些拆分表的邏輯表
筋帖。
真實(shí)表
真實(shí)表也就是上邊提到的 t_order_n
數(shù)據(jù)庫中真實(shí)存在的物理表。
分片鍵
用于分片的數(shù)據(jù)庫字段冤馏。我們將 t_order
表分片以后日麸,當(dāng)執(zhí)行一條SQL時(shí),通過對(duì)字段 order_id
取模的方式來決定宿接,這條數(shù)據(jù)該在哪個(gè)數(shù)據(jù)庫中的哪個(gè)表中執(zhí)行赘淮,此時(shí) order_id
字段就是 t_order
表的分片健。
這樣以來同一個(gè)訂單的相關(guān)數(shù)據(jù)就會(huì)存在同一個(gè)數(shù)據(jù)庫表中睦霎,大幅提升數(shù)據(jù)檢索的性能梢卸,不僅如此 sharding-jdbc
還支持根據(jù)多個(gè)字段作為分片健進(jìn)行分片。
分片算法
上邊我們提到可以用分片健取模的規(guī)則分片副女,但這只是比較簡(jiǎn)單的一種蛤高,在實(shí)際開發(fā)中我們還希望用 >=
、<=
碑幅、>
戴陡、<
、BETWEEN
和 IN
等條件作為分片規(guī)則沟涨,自定義分片邏輯恤批,這時(shí)就需要用到分片策略與分片算法。
從執(zhí)行 SQL 的角度來看裹赴,分庫分表可以看作是一種路由機(jī)制喜庞,把 SQL 語句路由到我們期望的數(shù)據(jù)庫或數(shù)據(jù)表中并獲取數(shù)據(jù)诀浪,分片算法可以理解成一種路由規(guī)則。
咱們先捋一下它們之間的關(guān)系延都,分片策略只是抽象出的概念雷猪,它是由分片算法和分片健組合而成,分片算法做具體的數(shù)據(jù)分片邏輯晰房。
分庫求摇、分表的分片策略配置是相對(duì)獨(dú)立的,可以各自使用不同的策略與算法殊者,每種策略中可以是多個(gè)分片算法的組合与境,每個(gè)分片算法可以對(duì)多個(gè)分片健做邏輯判斷。
注意:sharding-jdbc 并沒有直接提供分片算法的實(shí)現(xiàn)幽污,需要開發(fā)者根據(jù)業(yè)務(wù)自行實(shí)現(xiàn)嚷辅。
sharding-jdbc
提供了4種分片算法:
1、精確分片算法
精確分片算法(PreciseShardingAlgorithm)用于單個(gè)字段作為分片鍵距误,SQL中有 =
與 IN
等條件的分片,需要在標(biāo)準(zhǔn)分片策略(StandardShardingStrategy
)下使用扁位。
2准潭、范圍分片算法
范圍分片算法(RangeShardingAlgorithm)用于單個(gè)字段作為分片鍵,SQL中有 BETWEEN AND
域仇、>
刑然、<
、>=
暇务、<=
等條件的分片泼掠,需要在標(biāo)準(zhǔn)分片策略(StandardShardingStrategy
)下使用。
3垦细、復(fù)合分片算法
復(fù)合分片算法(ComplexKeysShardingAlgorithm)用于多個(gè)字段作為分片鍵的分片操作择镇,同時(shí)獲取到多個(gè)分片健的值,根據(jù)多個(gè)字段處理業(yè)務(wù)邏輯括改。需要在復(fù)合分片策略(ComplexShardingStrategy
)下使用腻豌。
4、Hint分片算法
Hint分片算法(HintShardingAlgorithm)稍有不同嘱能,上邊的算法中我們都是解析SQL
語句提取分片鍵吝梅,并設(shè)置分片策略進(jìn)行分片。但有些時(shí)候我們并沒有使用任何的分片鍵和分片策略惹骂,可還想將 SQL 路由到目標(biāo)數(shù)據(jù)庫和表苏携,就需要通過手動(dòng)干預(yù)指定SQL的目標(biāo)數(shù)據(jù)庫和表信息,這也叫強(qiáng)制路由对粪。
分片策略
上邊講分片算法的時(shí)候已經(jīng)說過右冻,分片策略是一種抽象的概念穿扳,實(shí)際分片操作的是由分片算法和分片健來完成的。
1国旷、標(biāo)準(zhǔn)分片策略
標(biāo)準(zhǔn)分片策略適用于單分片鍵矛物,此策略支持 PreciseShardingAlgorithm
和 RangeShardingAlgorithm
兩個(gè)分片算法。
其中 PreciseShardingAlgorithm
是必選的跪但,用于處理 =
和 IN
的分片履羞。RangeShardingAlgorithm
是可選的,用于處理BETWEEN AND
屡久, >
忆首, <
,>=
被环,<=
條件分片糙及,如果不配置RangeShardingAlgorithm
,SQL中的條件等將按照全庫路由處理筛欢。
2浸锨、復(fù)合分片策略
復(fù)合分片策略,同樣支持對(duì) SQL語句中的 =
版姑,>
柱搜, <
, >=
剥险, <=
聪蘸,IN
和 BETWEEN AND
的分片操作。不同的是它支持多分片鍵表制,具體分配片細(xì)節(jié)完全由應(yīng)用開發(fā)者實(shí)現(xiàn)健爬。
3、行表達(dá)式分片策略
行表達(dá)式分片策略么介,支持對(duì) SQL語句中的 =
和 IN
的分片操作娜遵,但只支持單分片鍵。這種策略通常用于簡(jiǎn)單的分片夭拌,不需要自定義分片算法魔熏,可以直接在配置文件中接著寫規(guī)則。
t_order_$->{t_order_id % 4}
代表 t_order
對(duì)其字段 t_order_id
取模鸽扁,拆分成4張表蒜绽,而表名分別是t_order_0
到 t_order_3
。
4桶现、Hint分片策略
Hint分片策略躲雅,對(duì)應(yīng)上邊的Hint分片算法,通過指定分片健而非從 SQL
中提取分片健的方式進(jìn)行分片的策略骡和。
分布式主鍵
數(shù)據(jù)分?后相赁,不同數(shù)據(jù)節(jié)點(diǎn)?成全局唯?主鍵是?常棘?的問題相寇,同?個(gè)邏輯表(t_order
)內(nèi)的不同真實(shí)表(t_order_n
)之間的?增鍵由于?法互相感知而產(chǎn)?重復(fù)主鍵。
盡管可通過設(shè)置?增主鍵 初始值
和 步?
的?式避免ID碰撞钮科,但這樣會(huì)使維護(hù)成本加大唤衫,乏完整性和可擴(kuò)展性。如果后去需要增加分片表的數(shù)量绵脯,要逐一修改分片表的步長(zhǎng)佳励,運(yùn)維成本非常高,所以不建議這種方式蛆挫。
實(shí)現(xiàn)分布式主鍵?成器的方式很多赃承,具體可以百度,網(wǎng)上有很多
為了讓上手更加簡(jiǎn)單悴侵,ApacheShardingSphere 內(nèi)置了UUID
瞧剖、SNOWFLAKE
兩種分布式主鍵?成器,默認(rèn)使?雪花算法(snowflake
)?成64bit的?整型數(shù)據(jù)可免。不僅如此它還抽離出分布式主鍵?成器的接口抓于,?便我們實(shí)現(xiàn)?定義的?增主鍵?成算法。
廣播表
廣播表:存在于所有的分片數(shù)據(jù)源中的表巴元,表結(jié)構(gòu)和表中的數(shù)據(jù)在每個(gè)數(shù)據(jù)庫中均完全一致毡咏。一般是為字典表或者配置表 t_config
,某個(gè)表一旦被配置為廣播表逮刨,只要修改某個(gè)數(shù)據(jù)庫的廣播表,所有數(shù)據(jù)源中廣播表的數(shù)據(jù)都會(huì)跟著同步堵泽。
綁定表
綁定表:那些分片規(guī)則一致的主表和子表修己。比如:t_order
訂單表和 t_order_item
訂單服務(wù)項(xiàng)目表,都是按 order_id
字段分片迎罗,因此兩張表互為綁定表關(guān)系睬愤。
那綁定表存在的意義是啥呢?
通常在我們的業(yè)務(wù)中都會(huì)使用 t_order
和 t_order_item
等表進(jìn)行多表聯(lián)合查詢纹安,但由于分庫分表以后這些表被拆分成N多個(gè)子表尤辱。如果不配置綁定表關(guān)系,會(huì)出現(xiàn)笛卡爾積關(guān)聯(lián)查詢厢岂,將產(chǎn)生如下四條SQL
光督。
SELECT * FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id
SELECT * FROM t_order_0 o JOIN t_order_item_1 i ON o.order_id=i.order_id
SELECT * FROM t_order_1 o JOIN t_order_item_0 i ON o.order_id=i.order_id
SELECT * FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id
而配置綁定表關(guān)系后再進(jìn)行關(guān)聯(lián)查詢時(shí),只要對(duì)應(yīng)表分片規(guī)則一致產(chǎn)生的數(shù)據(jù)就會(huì)落到同一個(gè)庫中塔粒,那么只需 t_order_0
和 t_order_item_0
表關(guān)聯(lián)即可结借。
SELECT * FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id
SELECT * FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id
注意:在關(guān)聯(lián)查詢時(shí)
t_order
它作為整個(gè)聯(lián)合查詢的主表。所有相關(guān)的路由計(jì)算都只使用主表的策略卒茬,t_order_item
表的分片相關(guān)的計(jì)算也會(huì)使用t_order
的條件船老,所以要保證綁定表之間的分片鍵要完全相同咖熟。
三、和JDBC的貓膩
從名字上不難看出柳畔,Sharding-JDBC
和 JDBC
有很大關(guān)系馍管,我們知道 JDBC 是一種 Java
語言訪問關(guān)系型數(shù)據(jù)庫的規(guī)范,其設(shè)計(jì)初衷就是要提供一套用于各種數(shù)據(jù)庫的統(tǒng)一標(biāo)準(zhǔn)薪韩,不同廠家共同遵守這套標(biāo)準(zhǔn)确沸,并提供各自的實(shí)現(xiàn)方案供應(yīng)用程序調(diào)用。
但其實(shí)對(duì)于開發(fā)人員而言躬存,我們只關(guān)心如何調(diào)用 JDBC API 來訪問數(shù)據(jù)庫张惹,只要正確使用 DataSource
、Connection
岭洲、Statement
宛逗、ResultSet
等 API 接口,直接操作數(shù)據(jù)庫即可盾剩。所以如果想在 JDBC 層面實(shí)現(xiàn)數(shù)據(jù)分片就必須對(duì)現(xiàn)有的 API 進(jìn)行功能拓展雷激,而 Sharding-JDBC 正是基于這種思想,重寫了 JDBC 規(guī)范并完全兼容了 JDBC 規(guī)范告私。
對(duì)原有的 DataSource
屎暇、Connection
等接口擴(kuò)展成 ShardingDataSource
、ShardingConnection
驻粟,而對(duì)外暴露的分片操作接口與 JDBC 規(guī)范中所提供的接口完全一致根悼,只要你熟悉 JDBC 就可以輕松應(yīng)用 Sharding-JDBC 來實(shí)現(xiàn)分庫分表。
因此它適用于任何基于 JDBC
的 ORM
框架蜀撑,如:JPA
挤巡, Hibernate
,Mybatis
酷麦,Spring JDBC Template
或直接使用的 JDBC矿卑。完美兼容任何第三方的數(shù)據(jù)庫連接池,如:DBCP
沃饶, C3P0
母廷, BoneCP
,Druid
糊肤, HikariCP
等琴昆,幾乎對(duì)主流關(guān)系型數(shù)據(jù)庫都支持。
那 Sharding-JDBC
又是如何拓展這些接口的呢轩褐?想知道答案我們就的從源碼入手了椎咧,下邊我們以 JDBC API 中的 DataSource
為例看看它是如何被重寫擴(kuò)展的。
數(shù)據(jù)源 DataSource
接口的核心作用就是獲取數(shù)據(jù)庫連接對(duì)象 Connection
,我們看其內(nèi)部提供了兩個(gè)獲取數(shù)據(jù)庫連接的方法 勤讽,并且繼承了 CommonDataSource
和 Wrapper
兩個(gè)接口蟋座。
public interface DataSource extends CommonDataSource, Wrapper {
/**
* <p>Attempts to establish a connection with the data source that
* this {@code DataSource} object represents.
* @return a connection to the data source
*/
Connection getConnection() throws SQLException;
/**
* <p>Attempts to establish a connection with the data source that
* this {@code DataSource} object represents.
* @param username the database user on whose behalf the connection is
* being made
* @param password the user's password
*/
Connection getConnection(String username, String password)
throws SQLException;
}
其中 CommonDataSource
是定義數(shù)據(jù)源的根接口這很好理解,而 Wrapper
接口則是拓展 JDBC 分片功能的關(guān)鍵脚牍。
由于數(shù)據(jù)庫廠商的不同向臀,他們可能會(huì)各自提供一些超越標(biāo)準(zhǔn) JDBC API 的擴(kuò)展功能,但這些功能非 JDBC 標(biāo)準(zhǔn)并不能直接使用诸狭,而 Wrapper
接口的作用就是把一個(gè)由第三方供應(yīng)商提供的券膀、非 JDBC 標(biāo)準(zhǔn)的接口包裝成標(biāo)準(zhǔn)接口,也就是適配器模式
驯遇。
既然講到了適配器模式就多啰嗦幾句芹彬,也方便后邊的理解。
適配器模式個(gè)種比較常用的設(shè)計(jì)模式叉庐,它的作用是將某個(gè)類的接口轉(zhuǎn)換成客戶端期望的另一個(gè)接口舒帮,使原本因接口不匹配(或者不兼容)而無法在一起工作的兩個(gè)類能夠在一起工作。比如用耳機(jī)聽音樂陡叠,我有個(gè)圓頭的耳機(jī)玩郊,可手機(jī)插孔卻是扁口的,如果我想要使用耳機(jī)聽音樂就必須借助一個(gè)轉(zhuǎn)接頭才可以枉阵,這個(gè)轉(zhuǎn)接頭就起到了適配作用译红。舉個(gè)栗子:假如我們
Target
接口中有hello()
和word()
兩個(gè)方法。
public interface Target {
void hello();
void world();
}
可由于接口版本迭代Target
接口的 word()
方法可能會(huì)被廢棄掉或不被支持兴溜,Adaptee
類的 greet()
方法將代替hello()
方法侦厚。
public class Adaptee {
public void greet(){
}
public void world(){
}
}
但此時(shí)舊版本仍然有大量 word()
方法被使用中,解決此事最好的辦法就是創(chuàng)建一個(gè)適配器Adapter
拙徽,這樣就適配了 Target
類假夺,解決了接口升級(jí)帶來的兼容性問題。
public class Adapter extends Adaptee implements Target {
@Override
public void world() {
}
@Override
public void hello() {
super.greet();
}
@Override
public void greet() {
}
}
而 Sharding-JDBC
提供的正是非 JDBC 標(biāo)準(zhǔn)的接口斋攀,所以它也提供了類似的實(shí)現(xiàn)方案,也使用到了 Wrapper
接口做數(shù)據(jù)分片功能的適配梧田。除了 DataSource 之外淳蔼,Connection、Statement裁眯、ResultSet 等核心對(duì)象也都繼承了這個(gè)接口鹉梨。
下面我們通過 ShardingDataSource
類源碼簡(jiǎn)單看下實(shí)現(xiàn)過程,下圖是繼承關(guān)系流程圖穿稳。
ShardingDataSource
類它在原 DataSource
基礎(chǔ)上做了功能拓展存皂,初始化時(shí)注冊(cè)了分片SQL路由包裝器、SQL重寫上下文和結(jié)果集處理引擎,還對(duì)數(shù)據(jù)源類型做了校驗(yàn)旦袋,因?yàn)樗瑫r(shí)支持多個(gè)不同類型的數(shù)據(jù)源骤菠。到這好像也沒看出如何適配,那接著向上看 ShardingDataSource
的繼承類 AbstractDataSourceAdapter
疤孕。
@Getter
public class ShardingDataSource extends AbstractDataSourceAdapter {
private final ShardingRuntimeContext runtimeContext;
/**
* 注冊(cè)路由商乎、SQl重寫上下文、結(jié)果集處理引擎
*/
static {
NewInstanceServiceLoader.register(RouteDecorator.class);
NewInstanceServiceLoader.register(SQLRewriteContextDecorator.class);
NewInstanceServiceLoader.register(ResultProcessEngine.class);
}
/**
* 初始化時(shí)校驗(yàn)數(shù)據(jù)源類型 并根據(jù)數(shù)據(jù)源 map祭阀、分片規(guī)則鹉戚、數(shù)據(jù)庫類型得到一個(gè)分片上下文,用來獲取數(shù)據(jù)庫連接
*/
public ShardingDataSource(final Map<String, DataSource> dataSourceMap, final ShardingRule shardingRule, final Properties props) throws SQLException {
super(dataSourceMap);
checkDataSourceType(dataSourceMap);
runtimeContext = new ShardingRuntimeContext(dataSourceMap, shardingRule, props, getDatabaseType());
}
private void checkDataSourceType(final Map<String, DataSource> dataSourceMap) {
for (DataSource each : dataSourceMap.values()) {
Preconditions.checkArgument(!(each instanceof MasterSlaveDataSource), "Initialized data sources can not be master-slave data sources.");
}
}
/**
* 數(shù)據(jù)庫連接
*/
@Override
public final ShardingConnection getConnection() {
return new ShardingConnection(getDataSourceMap(), runtimeContext, TransactionTypeHolder.get());
}
}
AbstractDataSourceAdapter
抽象類內(nèi)部主要獲取不同類型的數(shù)據(jù)源對(duì)應(yīng)的數(shù)據(jù)庫連接對(duì)象专控,實(shí)現(xiàn) AutoCloseable
接口是為在使用完資源后可以自動(dòng)將這些資源關(guān)閉(調(diào)用 close
方法)抹凳,那再看看繼承類 AbstractUnsupportedOperationDataSource
。
@Getter
public abstract class AbstractDataSourceAdapter extends AbstractUnsupportedOperationDataSource implements AutoCloseable {
private final Map<String, DataSource> dataSourceMap;
private final DatabaseType databaseType;
public AbstractDataSourceAdapter(final Map<String, DataSource> dataSourceMap) throws SQLException {
this.dataSourceMap = dataSourceMap;
databaseType = createDatabaseType();
}
public AbstractDataSourceAdapter(final DataSource dataSource) throws SQLException {
dataSourceMap = new HashMap<>(1, 1);
dataSourceMap.put("unique", dataSource);
databaseType = createDatabaseType();
}
private DatabaseType createDatabaseType() throws SQLException {
DatabaseType result = null;
for (DataSource each : dataSourceMap.values()) {
DatabaseType databaseType = createDatabaseType(each);
Preconditions.checkState(null == result || result == databaseType, String.format("Database type inconsistent with '%s' and '%s'", result, databaseType));
result = databaseType;
}
return result;
}
/**
* 不同數(shù)據(jù)源類型獲取數(shù)據(jù)庫連接
*/
private DatabaseType createDatabaseType(final DataSource dataSource) throws SQLException {
if (dataSource instanceof AbstractDataSourceAdapter) {
return ((AbstractDataSourceAdapter) dataSource).databaseType;
}
try (Connection connection = dataSource.getConnection()) {
return DatabaseTypes.getDatabaseTypeByURL(connection.getMetaData().getURL());
}
}
@Override
public final Connection getConnection(final String username, final String password) throws SQLException {
return getConnection();
}
@Override
public final void close() throws Exception {
close(dataSourceMap.keySet());
}
}
AbstractUnsupportedOperationDataSource
實(shí)現(xiàn)DataSource
接口并繼承了 WrapperAdapter
類伦腐,它內(nèi)部并沒有什么具體方法只起到橋接的作用赢底,但看著是不是和我們前邊講適配器模式的例子方式有點(diǎn)相似。
public abstract class AbstractUnsupportedOperationDataSource extends WrapperAdapter implements DataSource {
@Override
public final int getLoginTimeout() throws SQLException {
throw new SQLFeatureNotSupportedException("unsupported getLoginTimeout()");
}
@Override
public final void setLoginTimeout(final int seconds) throws SQLException {
throw new SQLFeatureNotSupportedException("unsupported setLoginTimeout(int seconds)");
}
}
WrapperAdapter
是一個(gè)包裝器的適配類蔗牡,實(shí)現(xiàn)了 JDBC 中的 Wrapper
接口颖系,其中有兩個(gè)核心方法 recordMethodInvocation
用于添加需要執(zhí)行的方法和參數(shù),而 replayMethodsInvocation
則將添加的這些方法和參數(shù)通過反射執(zhí)行辩越。仔細(xì)看不難發(fā)現(xiàn)兩個(gè)方法中都用到了 JdbcMethodInvocation
類嘁扼。
public abstract class WrapperAdapter implements Wrapper {
private final Collection<JdbcMethodInvocation> jdbcMethodInvocations = new ArrayList<>();
/**
* 添加要執(zhí)行的方法
*/
@SneakyThrows
public final void recordMethodInvocation(final Class<?> targetClass, final String methodName, final Class<?>[] argumentTypes, final Object[] arguments) {
jdbcMethodInvocations.add(new JdbcMethodInvocation(targetClass.getMethod(methodName, argumentTypes), arguments));
}
/**
* 通過反射執(zhí)行 上邊添加的方法
*/
public final void replayMethodsInvocation(final Object target) {
for (JdbcMethodInvocation each : jdbcMethodInvocations) {
each.invoke(target);
}
}
}
JdbcMethodInvocation
類主要應(yīng)用反射通過傳入的 method
方法和 arguments
參數(shù)執(zhí)行對(duì)應(yīng)的方法,這樣就可以通過 JDBC API 調(diào)用非 JDBC 方法了黔攒。
@RequiredArgsConstructor
public class JdbcMethodInvocation {
@Getter
private final Method method;
@Getter
private final Object[] arguments;
/**
* Invoke JDBC method.
*
* @param target target object
*/
@SneakyThrows
public void invoke(final Object target) {
method.invoke(target, arguments);
}
}
那 Sharding-JDBC
拓展 JDBC API 接口后趁啸,在新增的分片功能里又做了哪些事情呢?
一張表經(jīng)過分庫分表后被拆分成多個(gè)子表督惰,并分散到不同的數(shù)據(jù)庫中不傅,在不修改原業(yè)務(wù) SQL 的前提下,Sharding-JDBC
就必須對(duì) SQL進(jìn)行一些改造才能正常執(zhí)行赏胚。
大致的執(zhí)行流程:SQL 解析
-> 執(zhí)?器優(yōu)化
-> SQL 路由
-> SQL 改寫
-> SQL 執(zhí)?
-> 結(jié)果歸并
六步組成访娶,一起瞅瞅每個(gè)步驟做了點(diǎn)什么。
SQL 解析
SQL解析過程分為詞法解析和語法解析兩步觉阅,比如下邊這條查詢用戶訂單的SQL崖疤,先用詞法解析將SQL拆解成不可再分的原子單元。在根據(jù)不同數(shù)據(jù)庫方言所提供的字典典勇,將這些單元?dú)w類為關(guān)鍵字劫哼,表達(dá)式,變量或者操作符等類型割笙。
SELECT order_no,price FROM t_order_ where user_id = 10086 and order_status > 0
接著語法解析會(huì)將拆分后的SQL轉(zhuǎn)換為抽象語法樹权烧,通過對(duì)抽象語法樹遍歷,提煉出分片所需的上下文,上下文包含查詢字段信息(Field
)般码、表信息(Table
)妻率、查詢條件(Condition
)、排序信息(Order By
)侈询、分組信息(Group By
)以及分頁信息(Limit
)等舌涨,并標(biāo)記出 SQL中有可能需要改寫的位置。
執(zhí)?器優(yōu)化
執(zhí)?器優(yōu)化對(duì)SQL分片條件進(jìn)行優(yōu)化扔字,處理像關(guān)鍵字 OR
這種影響性能的壞味道囊嘉。
SQL 路由
SQL 路由通過解析分片上下文,匹配到用戶配置的分片策略革为,并生成路由路徑扭粱。簡(jiǎn)單點(diǎn)理解就是可以根據(jù)我們配置的分片策略計(jì)算出 SQL該在哪個(gè)庫的哪個(gè)表中執(zhí)行,而SQL路由又根據(jù)有無分片健區(qū)分出 分片路由
和 廣播路由
震檩。
有分?鍵的路由叫分片路由琢蛤,細(xì)分為直接路由、標(biāo)準(zhǔn)路由和笛卡爾積路由這3種類型抛虏。
標(biāo)準(zhǔn)路由
標(biāo)準(zhǔn)路由是最推薦也是最為常?的分??式博其,它的適?范圍是不包含關(guān)聯(lián)查詢或僅包含綁定表之間關(guān)聯(lián)查詢的SQL。
當(dāng) SQL分片健的運(yùn)算符為 =
時(shí)迂猴,路由結(jié)果將落?單庫(表)慕淡,當(dāng)分?運(yùn)算符是BETWEEN
或IN
等范圍時(shí),路由結(jié)果則不?定落?唯?的庫(表)沸毁,因此?條邏輯SQL最終可能被拆分為多條?于執(zhí)?的真實(shí)SQL峰髓。
SELECT * FROM t_order where t_order_id in (1,2)
SQL路由處理后
SELECT * FROM t_order_0 where t_order_id in (1,2)
SELECT * FROM t_order_1 where t_order_id in (1,2)
直接路由
直接路由是通過使用 HintAPI
直接將 SQL路由到指定?庫表的一種分?方式,而且直接路由可以?于分?鍵不在SQL中的場(chǎng)景息尺,還可以執(zhí)?包括?查詢携兵、?定義函數(shù)等復(fù)雜情況的任意SQL。
比如根據(jù) t_order_id
字段為條件查詢訂單搂誉,此時(shí)希望在不修改SQL的前提下徐紧,加上 user_id
作為分片條件就可以使用直接路由。
笛卡爾積路由
笛卡爾路由是由?綁定表之間的關(guān)聯(lián)查詢產(chǎn)生的炭懊,查詢性能較低盡量避免走此路由模式浪汪。
無分?鍵的路由又叫做廣播路由,可以劃分為全庫表路由凛虽、全庫路由、 全實(shí)例路由广恢、單播路由和阻斷路由這 5種類型凯旋。
全庫表路由
全庫表路由針對(duì)的是數(shù)據(jù)庫 DQL
和 DML
,以及 DDL
等操作,當(dāng)我們執(zhí)行一條邏輯表 t_order
SQL時(shí)至非,在所有分片庫中對(duì)應(yīng)的真實(shí)表 t_order_0
··· t_order_n
內(nèi)逐一執(zhí)行钠署。
全庫路由
全庫路由主要是對(duì)數(shù)據(jù)庫層面的操作,比如數(shù)據(jù)庫 SET
類型的數(shù)據(jù)庫管理命令荒椭,以及 TCL 這樣的事務(wù)控制語句谐鼎。
對(duì)邏輯庫設(shè)置 autocommit
屬性后,所有對(duì)應(yīng)的真實(shí)庫中都執(zhí)行該命令趣惠。
SET autocommit=0;
全實(shí)例路由
全實(shí)例路由是針對(duì)數(shù)據(jù)庫實(shí)例的 DCL 操作(設(shè)置或更改數(shù)據(jù)庫用戶或角色權(quán)限)狸棍,比如:創(chuàng)建一個(gè)用戶 order ,這個(gè)命令將在所有的真實(shí)庫實(shí)例中執(zhí)行味悄,以此確保 order 用戶可以正常訪問每一個(gè)數(shù)據(jù)庫實(shí)例草戈。
CREATE USER order@127.0.0.1 identified BY '程序員內(nèi)點(diǎn)事';
單播路由
單播路由用來獲取某一真實(shí)表信息,比如獲得表的描述信息:
DESCRIBE t_order;
t_order
的真實(shí)表是 t_order_0
···· t_order_n
侍瑟,他們的描述結(jié)構(gòu)相完全同唐片,我們只需在任意的真實(shí)表執(zhí)行一次就可以。
阻斷路由
?來屏蔽SQL對(duì)數(shù)據(jù)庫的操作涨颜,例如:
USE order_db;
這個(gè)命令不會(huì)在真實(shí)數(shù)據(jù)庫中執(zhí)?费韭,因?yàn)?ShardingSphere
采?的是邏輯 Schema(數(shù)據(jù)庫的組織和結(jié)構(gòu)) ?式,所以無需將切換數(shù)據(jù)庫的命令發(fā)送?真實(shí)數(shù)據(jù)庫中庭瑰。
SQL 改寫
將基于邏輯表開發(fā)的SQL改寫成可以在真實(shí)數(shù)據(jù)庫中可以正確執(zhí)行的語句星持。比如查詢 t_order
訂單表,我們實(shí)際開發(fā)中 SQL是按邏輯表 t_order
寫的见擦。
SELECT * FROM t_order
但分庫分表以后真實(shí)數(shù)據(jù)庫中 t_order
表就不存在了钉汗,而是被拆分成多個(gè)子表 t_order_n
分散在不同的數(shù)據(jù)庫內(nèi),還按原SQL執(zhí)行顯然是行不通的鲤屡,這時(shí)需要將分表配置中的邏輯表名稱改寫為路由之后所獲取的真實(shí)表名稱损痰。
SELECT * FROM t_order_n
SQL執(zhí)?
將路由和改寫后的真實(shí) SQL 安全且高效發(fā)送到底層數(shù)據(jù)源執(zhí)行。但這個(gè)過程并不是簡(jiǎn)單的將 SQL 通過JDBC 直接發(fā)送至數(shù)據(jù)源執(zhí)行酒来,而是平衡數(shù)據(jù)源連接創(chuàng)建以及內(nèi)存占用所產(chǎn)生的消耗卢未,它會(huì)自動(dòng)化的平衡資源控制與執(zhí)行效率。
結(jié)果歸并
將從各個(gè)數(shù)據(jù)節(jié)點(diǎn)獲取的多數(shù)據(jù)結(jié)果集堰汉,合并成一個(gè)大的結(jié)果集并正確的返回至請(qǐng)求客戶端辽社,稱為結(jié)果歸并。而我們SQL中的排序翘鸭、分組滴铅、分頁和聚合等語法,均是在歸并后的結(jié)果集上進(jìn)行操作的就乓。
四汉匙、快速實(shí)踐
下面我們結(jié)合 Springboot
+ mybatisplus
快速搭建一個(gè)分庫分表案例拱烁。
1、準(zhǔn)備工作
先做準(zhǔn)備工作噩翠,創(chuàng)建兩個(gè)數(shù)據(jù)庫 ds-0
戏自、ds-1
,兩個(gè)庫中分別建表 t_order_0
伤锚、t_order_1
擅笔、t_order_2
、t_order_item_0
屯援、t_order_item_1
猛们、t_order_item_2
,t_config
玄呛,方便后邊驗(yàn)證廣播表阅懦、綁定表的場(chǎng)景。
表結(jié)構(gòu)如下:
t_order_0
訂單表
CREATE TABLE `t_order_0` (
`order_id` bigint(200) NOT NULL,
`order_no` varchar(100) DEFAULT NULL,
`create_name` varchar(50) DEFAULT NULL,
`price` decimal(10,2) DEFAULT NULL,
PRIMARY KEY (`order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
t_order_0
與 t_order_item_0
互為關(guān)聯(lián)表
CREATE TABLE `t_order_item_0` (
`item_id` bigint(100) NOT NULL,
`order_no` varchar(200) NOT NULL,
`item_name` varchar(50) DEFAULT NULL,
`price` decimal(10,2) DEFAULT NULL,
PRIMARY KEY (`item_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
廣播表 t_config
`id` bigint(30) NOT NULL,
`remark` varchar(50) CHARACTER SET utf8 DEFAULT NULL,
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`last_modify_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
ShardingSphere
提供了4種分片配置方式:
Java 代碼配置
Yaml 徘铝、properties 配置
Spring 命名空間配置
Spring Boot配置
為讓代碼看上去更簡(jiǎn)潔和直觀耳胎,后邊統(tǒng)一使用 properties
配置的方式,引入 shardingsphere
對(duì)應(yīng)的 sharding-jdbc-spring-boot-starter
和 sharding-core-common
包惕它,版本統(tǒng)一用的 4.0.0-RC1怕午。
2、分片配置
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.0.0-RC1</version>
</dependency>
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-core-common</artifactId>
<version>4.0.0-RC1</version>
</dependency>
準(zhǔn)備工作做完( mybatis 搭建就不贅述了)淹魄,接下來我們逐一解讀分片配置信息郁惜。
我們首先定義兩個(gè)數(shù)據(jù)源 ds-0
、ds-1
甲锡,并分別加上數(shù)據(jù)源的基礎(chǔ)信息兆蕉。
# 定義兩個(gè)全局?jǐn)?shù)據(jù)源
spring.shardingsphere.datasource.names=ds-0,ds-1
# 配置數(shù)據(jù)源 ds-0
spring.shardingsphere.datasource.ds-0.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.ds-0.driverClassName=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds-0.url=jdbc:mysql://127.0.0.1:3306/ds-0?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT
spring.shardingsphere.datasource.ds-0.username=root
spring.shardingsphere.datasource.ds-0.password=root
# 配置數(shù)據(jù)源 ds-1
spring.shardingsphere.datasource.ds-1.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.ds-1.driverClassName=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds-1.url=jdbc:mysql://127.0.0.1:3306/ds-1?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT
spring.shardingsphere.datasource.ds-1.username=root
spring.shardingsphere.datasource.ds-1.password=root
配置完數(shù)據(jù)源接下來為表添加分庫和分表策略,使用 sharding-jdbc
做分庫分表需要我們?yōu)槊恳粋€(gè)表單獨(dú)設(shè)置分片規(guī)則缤沦。
# 配置分片表 t_order
# 指定真實(shí)數(shù)據(jù)節(jié)點(diǎn)
spring.shardingsphere.sharding.tables.t_order.actual-data-nodes=ds-$->{0..1}.t_order_$->{0..2}
actual-data-nodes
屬性指定分片的真實(shí)數(shù)據(jù)節(jié)點(diǎn)虎韵,$
是一個(gè)占位符,{0..1}表示實(shí)際拆分的數(shù)據(jù)庫表數(shù)量缸废。
ds-$->{0..1}.t_order_$->{0..2}
表達(dá)式相當(dāng)于 6個(gè)數(shù)據(jù)節(jié)點(diǎn)
- ds-0.t_order_0
- ds-0.t_order_1
- ds-0.t_order_2
- ds-1.t_order_0
- ds-1.t_order_1
- ds-1.t_order_2
### 分庫策略
# 分庫分片健
spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.sharding-column=order_id
# 分庫分片算法
spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.algorithm-expression=ds-$->{order_id % 2}
為表設(shè)置分庫策略包蓝,上邊講了 sharding-jdbc
它提供了四種分片策略,為快速搭建我們先以最簡(jiǎn)單的行內(nèi)表達(dá)式分片策略來實(shí)現(xiàn)企量,在下一篇會(huì)介紹四種分片策略的詳細(xì)用法和使用場(chǎng)景测萎。
database-strategy.inline.sharding-column
屬性中 database-strategy
為分庫策略,inline
為具體的分片策略届巩,sharding-column
代表分片健硅瞧。
database-strategy.inline.algorithm-expression
是當(dāng)前策略下具體的分片算法,ds-$->{order_id % 2}
表達(dá)式意思是 對(duì) order_id
字段進(jìn)行取模分庫恕汇,2 代表分片庫的個(gè)數(shù)零酪,不同的策略對(duì)應(yīng)不同的算法冒嫡,這里也可以是我們自定義的分片算法類。
# 分表策略
# 分表分片健
spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.sharding-column=order_id
# 分表算法
spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.algorithm-expression=t_order_$->{order_id % 3}
# 自增主鍵字段
spring.shardingsphere.sharding.tables.t_order.key-generator.column=order_id
# 自增主鍵ID 生成方案
spring.shardingsphere.sharding.tables.t_order.key-generator.type=SNOWFLAKE
分表策略 和 分庫策略 的配置比較相似四苇,不同的是分表可以通過 key-generator.column
和 key-generator.type
設(shè)置自增主鍵以及指定自增主鍵的生成方案,目前內(nèi)置了SNOWFLAKE
和 UUID
兩種方式方咆,還能自定義的主鍵生成算法類月腋,后續(xù)會(huì)詳細(xì)的講解。
# 綁定表關(guān)系
spring.shardingsphere.sharding.binding-tables= t_order,t_order_item
必須按相同分片健進(jìn)行分片的表才能互為成綁定表瓣赂,在聯(lián)合查詢時(shí)就能避免出現(xiàn)笛卡爾積查詢榆骚。
# 配置廣播表
spring.shardingsphere.sharding.broadcast-tables=t_config
廣播表,開啟 SQL解析日志煌集,能清晰的看到 SQL分片解析的過程
# 是否開啟 SQL解析日志
spring.shardingsphere.props.sql.show=true
3妓肢、驗(yàn)證分片
分片配置完以后我們無需在修改業(yè)務(wù)代碼了,直接執(zhí)行業(yè)務(wù)邏輯的增苫纤、刪碉钠、改、查即可卷拘,接下來驗(yàn)證一下分片的效果喊废。
我們同時(shí)向 t_order
、t_order_item
表插入 5條訂單記錄栗弟,并不給定主鍵 order_id
污筷,item_id
字段值。
public String insertOrder() {
for (int i = 0; i < 4; i++) {
TOrder order = new TOrder();
order.setOrderNo("A000" + i);
order.setCreateName("訂單 " + i);
order.setPrice(new BigDecimal("" + i));
orderRepository.insert(order);
TOrderItem orderItem = new TOrderItem();
orderItem.setOrderId(order.getOrderId());
orderItem.setOrderNo("A000" + i);
orderItem.setItemName("服務(wù)項(xiàng)目" + i);
orderItem.setPrice(new BigDecimal("" + i));
orderItemRepository.insert(orderItem);
}
return "success";
}
看到訂單記錄被成功分散到了不同的庫表中乍赫, order_id
字段也自動(dòng)生成了主鍵ID瓣蛀,基礎(chǔ)的分片功能就完成了。
那向廣播表 t_config
中插入一條數(shù)據(jù)會(huì)是什么效果呢雷厂?
public String config() {
TConfig tConfig = new TConfig();
tConfig.setRemark("我是廣播表");
tConfig.setCreateTime(new Date());
tConfig.setLastModifyTime(new Date());
configRepository.insert(tConfig);
return "success";
}
發(fā)現(xiàn)所有庫中 t_config
表都執(zhí)行了這條SQL惋增,廣播表和 MQ廣播訂閱的模式很相似,所有訂閱的客戶端都會(huì)收到同一條消息罗侯。
簡(jiǎn)單SQL操作驗(yàn)證沒問通器腋,接下來在試試復(fù)雜一點(diǎn)的聯(lián)合查詢,前邊我們已經(jīng)把 t_order
钩杰、t_order_item
表設(shè)為綁定表慧瘤,直接聯(lián)表查詢執(zhí)行一下晦鞋。
通過控制臺(tái)日志發(fā)現(xiàn),邏輯表SQL 經(jīng)過解析以后,只對(duì) t_order_0
和 t_order_item_0
表進(jìn)行了關(guān)聯(lián)產(chǎn)生一條SQL厢绝。
那如果不互為綁定表又會(huì)是什么情況呢?去掉 spring.shardingsphere.sharding.binding-tables
試一下二蓝。
發(fā)現(xiàn)控制臺(tái)解析出了 3條真實(shí)表SQL,而去掉 order_id
作為查詢條件再次執(zhí)行后胸嘁,結(jié)果解析出了 9條SQL,進(jìn)行了笛卡爾積查詢凉逛。所以相比之下綁定表的優(yōu)點(diǎn)就不言而喻了性宏。
五、總結(jié)
以上對(duì)分庫分表中間件 sharding-jdbc
的基礎(chǔ)概念做了簡(jiǎn)單梳理状飞,快速的搭建了一個(gè)分庫分表案例毫胜,但這只是實(shí)踐分庫分表的第一步,下一篇我們會(huì)詳細(xì)的介紹四種分片策略的具體用法和使用場(chǎng)景(必知必會(huì))诬辈,后邊將陸續(xù)講解自定義分布式主鍵酵使、分布式數(shù)據(jù)庫事務(wù)、分布式服務(wù)治理焙糟,數(shù)據(jù)脫敏等口渔。
寫在最后
歡迎大家關(guān)注我的公眾號(hào)【風(fēng)平浪靜如碼】,海量Java相關(guān)文章穿撮,學(xué)習(xí)資料都會(huì)在里面更新缺脉,整理的資料也會(huì)放在里面。
覺得寫的還不錯(cuò)的就點(diǎn)個(gè)贊混巧,加個(gè)關(guān)注唄枪向!點(diǎn)關(guān)注,不迷路咧党,持續(xù)更新C鼗住!傍衡!