分庫(kù)分表Sharding-JDBC入門與項(xiàng)目實(shí)戰(zhàn),數(shù)據(jù)量大了一定要分表 !

最近項(xiàng)目中不少表的數(shù)據(jù)量越來(lái)越大,并且導(dǎo)致了一些數(shù)據(jù)庫(kù)的性能問(wèn)題。因此想借助一些分庫(kù)分表的中間件,實(shí)現(xiàn)自動(dòng)化分庫(kù)分表實(shí)現(xiàn)。調(diào)研下來(lái),發(fā)現(xiàn)Sharding-JDBC目前成熟度最高并且應(yīng)用最廣的Java分庫(kù)分表的客戶端組件。本文主要介紹一些Sharding-JDBC核心概念以及生產(chǎn)環(huán)境下的實(shí)戰(zhàn)指南,旨在幫助組內(nèi)成員快速了解Sharding-JDBC并且能夠快速將其使用起來(lái)。
核心概念
在使用Sharding-JDBC之前,一定是先理解清楚下面幾個(gè)核心概念。
邏輯表
水平拆分的數(shù)據(jù)庫(kù)(表)的相同邏輯和數(shù)據(jù)結(jié)構(gòu)表的總稱。例:訂單數(shù)據(jù)根據(jù)主鍵尾數(shù)拆分為10張表,分別是t_order_0到t_order_9,他們的邏輯表名為t_order。
真實(shí)表
在分片的數(shù)據(jù)庫(kù)中真實(shí)存在的物理表。即上個(gè)示例中的t_order_0到t_order_9。
數(shù)據(jù)節(jié)點(diǎn)
數(shù)據(jù)分片的最小單元。由數(shù)據(jù)源名稱和數(shù)據(jù)表組成,例:ds_0.t_order_0。
綁定表
指分片規(guī)則一致的主表和子表。例如:t_order表和t_order_item表,均按照order_id分片,則此兩張表互為綁定表關(guān)系。綁定表之間的多表關(guān)聯(lián)查詢不會(huì)出現(xiàn)笛卡爾積關(guān)聯(lián),關(guān)聯(liá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);假設(shè)t_order和t_order_item對(duì)應(yīng)的真實(shí)表各有2個(gè),那么真實(shí)表就有t_order_0、t_order_1、t_order_item_0、t_order_item_1。在不配置綁定表關(guān)系時(shí),假設(shè)分片鍵order_id將數(shù)值10路由至第0片,將數(shù)值11路由至第1片,那么路由后的SQL應(yīng)該為4條,它們呈現(xiàn)為笛卡爾積:
SELECT?i.* FROM?t_order_0 o JOIN?t_order_item_0 i ON?o.order_id=i.order_id WHERE?o.order_id in?(10, 11);
SELECT?i.* FROM?t_order_0 o JOIN?t_order_item_1 i ON?o.order_id=i.order_id WHERE?o.order_id in?(10, 11);
SELECT?i.* FROM?t_order_1 o JOIN?t_order_item_0 i ON?o.order_id=i.order_id WHERE?o.order_id in?(10, 11);
SELECT?i.* FROM?t_order_1 o JOIN?t_order_item_1 i ON?o.order_id=i.order_id WHERE?o.order_id in?(10, 11);在配置綁定表關(guān)系后,路由的SQL應(yīng)該為2條:
SELECT?i.* FROM?t_order_0 o JOIN?t_order_item_0 i ON?o.order_id=i.order_id WHERE?o.order_id in?(10, 11);
SELECT?i.* FROM?t_order_1 o JOIN?t_order_item_1 i ON?o.order_id=i.order_id WHERE?o.order_id in?(10, 11);廣播表
指所有的分片數(shù)據(jù)源中都存在的表,表結(jié)構(gòu)和表中的數(shù)據(jù)在每個(gè)數(shù)據(jù)庫(kù)中均完全一致。適用于數(shù)據(jù)量不大且需要與海量數(shù)據(jù)的表進(jìn)行關(guān)聯(lián)查詢的場(chǎng)景,例如:字典表。
數(shù)據(jù)分片分片鍵
用于分片的數(shù)據(jù)庫(kù)字段,是將數(shù)據(jù)庫(kù)(表)水平拆分的關(guān)鍵字段。例:將訂單表中的訂單主鍵的尾數(shù)取模分片,則訂單主鍵為分片字段。SQL 中如果無(wú)分片字段,將執(zhí)行全路由,性能較差。除了對(duì)單分片字段的支持,Sharding-JDBC 也支持根據(jù)多個(gè)字段進(jìn)行分片。
分片算法
通過(guò)分片算法將數(shù)據(jù)分片,支持通過(guò)=、>=、<=、>、<、BETWEEN和IN分片。分片算法需要應(yīng)用方開發(fā)者自行實(shí)現(xiàn),可實(shí)現(xiàn)的靈活度非常高。
目前提供4種分片算法。由于分片算法和業(yè)務(wù)實(shí)現(xiàn)緊密相關(guān),因此并未提供內(nèi)置分片算法,而是通過(guò)分片策略將各種場(chǎng)景提煉出來(lái),提供更高層級(jí)的抽象,并提供接口讓應(yīng)用開發(fā)者自行實(shí)現(xiàn)分片算法。
精確分片算法
對(duì)應(yīng) PreciseShardingAlgorithm,用于處理使用單一鍵作為分片鍵的 = 與 IN 進(jìn)行分片的場(chǎng)景。需要配合 StandardShardingStrategy 使用。
范圍分片算法
對(duì)應(yīng) RangeShardingAlgorithm,用于處理使用單一鍵作為分片鍵的 BETWEEN AND、>、<、>=、<=進(jìn)行分片的場(chǎng)景。需要配合 StandardShardingStrategy 使用。
復(fù)合分片算法
對(duì)應(yīng) ComplexKeysShardingAlgorithm,用于處理使用多鍵作為分片鍵進(jìn)行分片的場(chǎng)景,包含多個(gè)分片鍵的邏輯較復(fù)雜,需要應(yīng)用開發(fā)者自行處理其中的復(fù)雜度。需要配合 ComplexShardingStrategy 使用。
Hint分片算法
對(duì)應(yīng) HintShardingAlgorithm,用于處理通過(guò)Hint指定分片值而非從SQL中提取分片值的場(chǎng)景。需要配合 HintShardingStrategy 使用。
分片策略
包含分片鍵和分片算法,由于分片算法的獨(dú)立性,將其獨(dú)立抽離。真正可用于分片操作的是分片鍵 + 分片算法,也就是分片策略。目前提供 5 種分片策略。
標(biāo)準(zhǔn)分片策略
對(duì)應(yīng) StandardShardingStrategy。提供對(duì) SQ L語(yǔ)句中的 =, >, <, >=, <=, IN 和 BETWEEN AND 的分片操作支持。StandardShardingStrategy 只支持單分片鍵,提供 PreciseShardingAlgorithm 和 RangeShardingAlgorithm 兩個(gè)分片算法。PreciseShardingAlgorithm 是必選的,用于處理 = 和 IN 的分片。RangeShardingAlgorithm 是可選的,用于處理 BETWEEN AND, >, <, >=, <=分片,如果不配置 RangeShardingAlgorithm,SQL 中的 BETWEEN AND 將按照全庫(kù)路由處理。
復(fù)合分片策略
對(duì)應(yīng) ComplexShardingStrategy。復(fù)合分片策略。提供對(duì) SQL 語(yǔ)句中的 =, >, <, >=, <=, IN 和 BETWEEN AND 的分片操作支持。ComplexShardingStrategy 支持多分片鍵,由于多分片鍵之間的關(guān)系復(fù)雜,因此并未進(jìn)行過(guò)多的封裝,而是直接將分片鍵值組合以及分片操作符透?jìng)髦练制惴?,完全由?yīng)用開發(fā)者實(shí)現(xiàn),提供最大的靈活度。
行表達(dá)式分片策略
對(duì)應(yīng) InlineShardingStrategy。使用 Groovy 的表達(dá)式,提供對(duì) SQL 語(yǔ)句中的 = 和 IN的分片操作支持,只支持單分片鍵。對(duì)于簡(jiǎn)單的分片算法,可以通過(guò)簡(jiǎn)單的配置使用,從而避免繁瑣的Java代碼開發(fā),如: t_user_$->{u_id % 8} 表示 t_user 表根據(jù) u_id 模 8,而分成 8 張表,表名稱為 t_user_0 到 t_user_7。可以認(rèn)為是精確分片算法的簡(jiǎn)易實(shí)現(xiàn)
Hint分片策略
對(duì)應(yīng) HintShardingStrategy。通過(guò) Hint 指定分片值而非從 SQL 中提取分片值的方式進(jìn)行分片的策略。
分布式主鍵
用于在分布式環(huán)境下,生成全局唯一的id。Sharding-JDBC 提供了內(nèi)置的分布式主鍵生成器,例如 UUID、SNOWFLAKE。還抽離出分布式主鍵生成器的接口,方便用戶自行實(shí)現(xiàn)自定義的自增主鍵生成器。為了保證數(shù)據(jù)庫(kù)性能,主鍵id還必須趨勢(shì)遞增,避免造成頻繁的數(shù)據(jù)頁(yè)面分裂。
讀寫分離
提供一主多從的讀寫分離配置,可獨(dú)立使用,也可配合分庫(kù)分表使用。
同一線程且同一數(shù)據(jù)庫(kù)連接內(nèi),如有寫入操作,以后的讀操作均從主庫(kù)讀取,用于保證數(shù)據(jù)一致性
基于Hint的強(qiáng)制主庫(kù)路由。
主從模型中,事務(wù)中讀寫均用主庫(kù)。
執(zhí)行流程
Sharding-JDBC 的原理總結(jié)起來(lái)很簡(jiǎn)單: 核心由 SQL解析 => 執(zhí)行器優(yōu)化 => SQL路由 => SQL改寫 => SQL執(zhí)行 => 結(jié)果歸并的流程組成。

項(xiàng)目實(shí)戰(zhàn)
spring-boot項(xiàng)目實(shí)戰(zhàn)
引入依賴
<dependency>
????<groupId>org.apache.shardingspheregroupId>
????<artifactId>sharding-jdbc-spring-boot-starterartifactId>
????<version>4.0.1version>
dependency>數(shù)據(jù)源配置
如果使用sharding-jdbc-spring-boot-starter, 并且數(shù)據(jù)源以及數(shù)據(jù)分片都使用shardingsphere進(jìn)行配置,對(duì)應(yīng)的數(shù)據(jù)源會(huì)自動(dòng)創(chuàng)建并注入到spring容器中。
spring.shardingsphere.datasource.names=ds0,ds1
spring.shardingsphere.datasource.ds0.type=org.apache.commons.dbcp.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=
spring.shardingsphere.datasource.ds1.type=org.apache.commons.dbcp.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=
# 其它分片配置但是在我們已有的項(xiàng)目中,數(shù)據(jù)源配置是單獨(dú)的。因此要禁用sharding-jdbc-spring-boot-starter里面的自動(dòng)裝配,而是參考源碼自己重寫數(shù)據(jù)源配置。需要在啟動(dòng)類上加上
@SpringBootApplication(exclude?= {org.apache.shardingsphere.shardingjdbc.spring.boot.SpringBootConfiguration.class})來(lái)排除。然后自定義配置類來(lái)裝配DataSource。
@Configuration
@Slf4j
@EnableConfigurationProperties({
????????SpringBootShardingRuleConfigurationProperties.class,
????????SpringBootMasterSlaveRuleConfigurationProperties.class, SpringBootEncryptRuleConfigurationProperties.class, SpringBootPropertiesConfigurationProperties.class})
@AutoConfigureBefore(DataSourceConfiguration.class)
public?class?DataSourceConfig?implements?ApplicationContextAware?{
????@Autowired
????private?SpringBootShardingRuleConfigurationProperties shardingRule;
????@Autowired
????private?SpringBootPropertiesConfigurationProperties props;
????private?ApplicationContext applicationContext;
????@Bean("shardingDataSource")
????@Conditional(ShardingRuleCondition.class)
????public?DataSource shardingDataSource()?throws?SQLException {
????????// 獲取其它方式配置的數(shù)據(jù)源
????????Map beans = applicationContext.getBeansOfType(DruidDataSourceWrapper.class);
????????Map dataSourceMap = new?HashMap<>(4);
????????beans.forEach(dataSourceMap::put);
????????// 創(chuàng)建shardingDataSource
????????return?ShardingDataSourceFactory.createDataSource(dataSourceMap, new?ShardingRuleConfigurationYamlSwapper().swap(shardingRule), props.getProps());
????}
????@Bean
????public?SqlSessionFactory sqlSessionFactory()?throws?SQLException {
????????SqlSessionFactoryBean sqlSessionFactoryBean = new?SqlSessionFactoryBean();
????????// 將shardingDataSource設(shè)置到SqlSessionFactory中
????????sqlSessionFactoryBean.setDataSource(shardingDataSource());
????????// 其它設(shè)置
????????return?sqlSessionFactoryBean.getObject();
????}
} 分布式id生成器配置
Sharding-JDBC提供了UUID、SNOWFLAKE生成器,還支持用戶實(shí)現(xiàn)自定義id生成器。比如可以實(shí)現(xiàn)了type為SEQ的分布式id生成器,調(diào)用統(tǒng)一的分布式id服務(wù)獲取id。
@Data
public?class?SeqShardingKeyGenerator?implements?ShardingKeyGenerator?{
????private?Properties properties = new?Properties();
????@Override
????public?String getType()?{
????????return?"SEQ";
????}
????@Override
????public?synchronized?Comparable> generateKey() {
???????// 獲取分布式id邏輯
????}
}由于擴(kuò)展ShardingKeyGenerator是通過(guò)JDK的serviceloader的SPI機(jī)制實(shí)現(xiàn)的,因此還需要在resources/META-INF/services目錄下配置org.apache.shardingsphere.spi.keygen.ShardingKeyGenerator文件。 文件內(nèi)容就是SeqShardingKeyGenerator類的全路徑名。這樣使用的時(shí)候,指定分布式主鍵生成器的type為SEQ就好了。
至此,Sharding-JDBC就整合進(jìn)spring-boot項(xiàng)目中了,后面就可以進(jìn)行數(shù)據(jù)分片相關(guān)的配置了。
數(shù)據(jù)分片實(shí)戰(zhàn)
如果項(xiàng)目初期就能預(yù)估出表的數(shù)據(jù)量級(jí),當(dāng)然可以一開始就按照這個(gè)預(yù)估值進(jìn)行分庫(kù)分表處理。但是大多數(shù)情況下,我們一開始并不能準(zhǔn)備預(yù)估出數(shù)量級(jí)。這時(shí)候通常的做法是:
線上數(shù)據(jù)某張表查詢性能開始下降,排查下來(lái)是因?yàn)閿?shù)據(jù)量過(guò)大導(dǎo)致的。
根據(jù)歷史數(shù)據(jù)量預(yù)估出未來(lái)的數(shù)據(jù)量級(jí),并結(jié)合具體業(yè)務(wù)場(chǎng)景確定分庫(kù)分表策略。
自動(dòng)分庫(kù)分表代碼實(shí)現(xiàn)。
下面就以一個(gè)具體事例,闡述具體數(shù)據(jù)分片實(shí)戰(zhàn)。比如有張表數(shù)據(jù)結(jié)構(gòu)如下:
CREATE?TABLE?`hc_question_reply_record`?(
??`id`?bigint?NOT?NULL?AUTO_INCREMENT COMMENT?'自增ID',
??`reply_text`?varchar(500) NOT?NULL?DEFAULT?''?COMMENT?'回復(fù)內(nèi)容',
??`reply_wheel_time`?datetime NOT?NULL?DEFAULT?CURRENT_TIMESTAMP?COMMENT?'回復(fù)時(shí)間',
??`ctime`?datetime NOT?NULL?DEFAULT?CURRENT_TIMESTAMP?COMMENT?'創(chuàng)建時(shí)間',
??`mtime`?datetime NOT?NULL?DEFAULT?CURRENT_TIMESTAMP?ON?UPDATE?CURRENT_TIMESTAMP?COMMENT?'更新時(shí)間',
??PRIMARY KEY?(`id`),
??INDEX?`idx_reply_wheel_time`?(`reply_wheel_time`)
) ENGINE=InnoDB?DEFAULT?CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
??COMMENT='回復(fù)明細(xì)記錄';分片方案確定
先查詢目前目標(biāo)表月新增趨勢(shì):
SELECT?count(*), date_format(ctime, '%Y-%m') AS?`日期`
FROM?hc_question_reply_record
GROUP?BY?date_format(ctime, '%Y-%m');
目前月新增在180w左右,預(yù)估未來(lái)達(dá)到300w(基本以2倍計(jì)算)以上。期望單表數(shù)據(jù)量不超過(guò)1000w,可使用reply_wheel_time作為分片鍵按季度歸檔。
分片配置spring:
spring:
??# sharing-jdbc配置
??shardingsphere:
????# 數(shù)據(jù)源名稱
????datasource:
??????names:?defaultDataSource,slaveDataSource
????sharding:
??????# 主從節(jié)點(diǎn)配置
??????master-slave-rules:
????????defaultDataSource:
??????????# maser數(shù)據(jù)源
??????????master-data-source-name:?defaultDataSource
??????????# slave數(shù)據(jù)源
??????????slave-data-source-names:?slaveDataSource
??????tables:
????????# hc_question_reply_record 分庫(kù)分表配置
????????hc_question_reply_record:
??????????# 真實(shí)數(shù)據(jù)節(jié)點(diǎn) hc_question_reply_record_2020_q1
??????????actual-data-nodes:?defaultDataSource.hc_question_reply_record_$->{2020..2025}_q$->{1..4}
??????????# 表分片策略
??????????table-strategy:
????????????standard:
??????????????# 分片鍵
??????????????sharding-column:?reply_wheel_time
??????????????# 精確分片算法 全路徑名
??????????????preciseAlgorithmClassName:?com.xx.QuestionRecordPreciseShardingAlgorithm
??????????????# 范圍分片算法,用于BETWEEN,可選。。該類需實(shí)現(xiàn)RangeShardingAlgorithm接口并提供無(wú)參數(shù)的構(gòu)造器
??????????????rangeAlgorithmClassName:?com.xx.QuestionRecordRangeShardingAlgorithm
??????# 默認(rèn)分布式id生成器
??????default-key-generator:
????????type:?SEQ
????????column:?id分片算法實(shí)現(xiàn)
精確分片算法:
public?class?QuestionRecordPreciseShardingAlgorithm?implements?PreciseShardingAlgorithm<Date> {
??/**
???* Sharding.
???*
???* @param?availableTargetNames available data sources or tables's names
???* @param?shardingValue sharding value
???* @return?sharding result for data source or table's name
???*/
??@Override
??public?String doSharding(Collection availableTargetNames, PreciseShardingValue shardingValue) ?{
??????return?ShardingUtils.quarterPreciseSharding(availableTargetNames, shardingValue);
??}
}范圍分片算法:
public?class?QuestionRecordRangeShardingAlgorithm?implements?RangeShardingAlgorithm<Date> {
??/**
???* Sharding.
???*
???* @param?availableTargetNames available data sources or tables's names
???* @param?shardingValue sharding value
???* @return?sharding results for data sources or tables's names
???*/
??@Override
??public?Collection doSharding(Collection availableTargetNames, RangeShardingValue shardingValue) ? {
??????return?ShardingUtils.quarterRangeSharding(availableTargetNames, shardingValue);
??}
}具體分片實(shí)現(xiàn)邏輯:
@UtilityClass
public?class?ShardingUtils?{
????public?static?final?String QUARTER_SHARDING_PATTERN = "%s_%d_q%d";
????/**
????* logicTableName_{year}_q{quarter}
????* 按季度范圍分片
????* @param?availableTargetNames 可用的真實(shí)表集合
????* @param?shardingValue 分片值
????* @return
????*/
????public?Collection quarterRangeSharding(Collection availableTargetNames, RangeShardingValue shardingValue) ? {
????????// 這里就是根據(jù)范圍查詢條件,篩選出匹配的真實(shí)表集合
????}
????/**
????* logicTableName_{year}_q{quarter}
????* 按季度精確分片
????* @param?availableTargetNames 可用的真實(shí)表集合
????* @param?shardingValue 分片值
????* @return
????*/
????public?static?String quarterPreciseSharding(Collection availableTargetNames, PreciseShardingValue shardingValue) ?{
????????// 這里就是根據(jù)等值查詢條件,計(jì)算出匹配的真實(shí)表
????}
}到這里,針對(duì)hc_question_reply_record表,使用reply_wheel_time作為分片鍵,按照季度分片的處理就完成了。還有一點(diǎn)要注意的就是,分庫(kù)分表之后,查詢的時(shí)候最好都帶上分片鍵作為查詢條件,否則就會(huì)使用全庫(kù)路由,性能很低。還有就是Sharing-JDBC對(duì)mysql的全文索引支持的不是很好,項(xiàng)目有使用到的地方也要注意一下??偨Y(jié)來(lái)說(shuō)整個(gè)過(guò)程還是比較簡(jiǎn)單的,后續(xù)碰到其它業(yè)務(wù)場(chǎng)景,相信大家按照這個(gè)思路肯定都能解決的。
原文鏈接:cnblogs.com/chentianming/p/13414237.html
