SpringBoot集成Sharding Jdbc使用復(fù)合分片的實(shí)踐
最近主要的工作重心是數(shù)據(jù)庫(kù)的容量規(guī)劃。
隨著業(yè)務(wù)的逐漸增大,原有保存在單表的數(shù)據(jù)量也日益增強(qiáng)。數(shù)據(jù)庫(kù)數(shù)據(jù)會(huì)隨著業(yè)務(wù)的發(fā)展而不斷增多,因此數(shù)據(jù)操作,如增刪改查的開(kāi)銷(xiāo)也會(huì)越來(lái)越大。再加上物理服務(wù)器的資源有限(CPU、磁盤(pán)、內(nèi)存、IO 等)。最終數(shù)據(jù)庫(kù)所能承載的數(shù)據(jù)量、數(shù)據(jù)處理能力都將遭遇瓶頸。換句話說(shuō)需要合理的數(shù)據(jù)庫(kù)架構(gòu)來(lái)存放不斷增長(zhǎng)的數(shù)據(jù),這個(gè)就是分庫(kù)分表的設(shè)計(jì)初衷。目的就是為了緩解數(shù)據(jù)庫(kù)的壓力,大限度提高數(shù)據(jù)操作的效率。
數(shù)據(jù)庫(kù)分庫(kù)分表中間件是采用的 apache sharding。
1、Sharing JDBC 簡(jiǎn)介
ShardingSphere是一套開(kāi)源的分布式數(shù)據(jù)庫(kù)中間件解決方案組成的生態(tài)圈,它由Sharding-JDBC、Sharding-Proxy和Sharding-Sidecar(計(jì)劃中)這3款相互獨(dú)立的產(chǎn)品組成。 他們均提供標(biāo)準(zhǔn)化的數(shù)據(jù)分片、分布式事務(wù)和數(shù)據(jù)庫(kù)治理功能,可適用于如Java同構(gòu)、異構(gòu)語(yǔ)言、云原生等各種多樣化的應(yīng)用場(chǎng)景。
ShardingSphere定位為關(guān)系型數(shù)據(jù)庫(kù)中間件,旨在充分合理地在分布式的場(chǎng)景下利用關(guān)系型數(shù)據(jù)庫(kù)的計(jì)算和存儲(chǔ)能力,而并非實(shí)現(xiàn)一個(gè)全新的關(guān)系型數(shù)據(jù)庫(kù)。 它與NoSQL和NewSQL是并存而非互斥的關(guān)系。NoSQL和NewSQL作為新技術(shù)探索的前沿,放眼未來(lái),擁抱變化,是非常值得推薦的。反之,也可以用另一種思路看待問(wèn)題,放眼未來(lái),關(guān)注不變的東西,進(jìn)而抓住事物本質(zhì)。 關(guān)系型數(shù)據(jù)庫(kù)當(dāng)今依然占有巨大市場(chǎng),是各個(gè)公司核心業(yè)務(wù)的基石,未來(lái)也難于撼動(dòng),我們目前階段更加關(guān)注在原有基礎(chǔ)上的增量,而非顛覆。
ShardingSphere已經(jīng)在2020年4月16日從Apache孵化器畢業(yè),成為Apache頂級(jí)項(xiàng)目。
2、系統(tǒng)改造
因?yàn)槲覀児緦儆诘谌街Ц镀脚_(tái),這個(gè)改造的點(diǎn)可以分為兩類(lèi):提供給商戶調(diào)用的對(duì)接系統(tǒng)(比如收銀臺(tái)),系統(tǒng)內(nèi)部調(diào)用的系統(tǒng)(支付引擎)。
- 收銀臺(tái)系統(tǒng):核心功能是提供給商戶提交交易訂單,并且對(duì)這筆交易訂單進(jìn)行支付的支付訂單
- 支付引擎:接收這個(gè)支付產(chǎn)品的請(qǐng)求,調(diào)用渠道,記賬,結(jié)算等功能
數(shù)據(jù)源使用分庫(kù)分表,在 Sharding JDBC 當(dāng)中如果進(jìn)行 修改、刪除、查詢操作中沒(méi)有包含分片鍵就會(huì)進(jìn)行全表掃描。所以在進(jìn)行業(yè)務(wù)改造的時(shí)候?qū)υ械臄?shù)據(jù)庫(kù)操作都進(jìn)行了業(yè)務(wù)優(yōu)化,基本改造后的所有的操作都使用了基于分片鍵進(jìn)行操作(定時(shí)任務(wù)除外)。
2.1 對(duì)接外部系統(tǒng)的系統(tǒng)
首先討論一下,提供給商戶調(diào)用的系統(tǒng)。在進(jìn)行下單操作的時(shí)候,商戶必須傳遞商戶號(hào)和外部訂單號(hào)。對(duì)于外部訂單號(hào)第三方支付系統(tǒng)無(wú)法控制,只需要商戶每次傳遞過(guò)來(lái)的時(shí)候與歷史的外部訂單號(hào)不重復(fù)就可以了。所以這里就涉及到一張映射表,這個(gè)表的主要功能如下:
- 把商戶的外部訂單號(hào)映射成內(nèi)部訂單號(hào)
- 通過(guò)商戶號(hào)與商戶的外部訂單號(hào)在數(shù)據(jù)庫(kù)聯(lián)合唯一達(dá)到冪等處理
- 保存商戶請(qǐng)求的原始數(shù)據(jù),做為請(qǐng)求憑證
這個(gè)時(shí)候?qū)灰子唵尉鸵蕾囉谕獠坑成浔恚颜?qǐng)求映射成內(nèi)部訂單號(hào)進(jìn)行分片就可以了
2.2 內(nèi)部系統(tǒng)間的調(diào)用
當(dāng)商戶下好了交易訂單的時(shí)候,需要進(jìn)行支付這個(gè)時(shí)候就產(chǎn)生了一筆支付訂單。交易訂單和支付訂單是一對(duì)多的關(guān)系。當(dāng)用戶進(jìn)行支付的時(shí)候會(huì)調(diào)用支付引擎,這個(gè)時(shí)候正常情況下一般會(huì)生成支付系統(tǒng)的支付訂單。然后支付引擎會(huì)調(diào)用后續(xù)的渠道、結(jié)算、記賬等系統(tǒng),系統(tǒng)之間的調(diào)用圖如下:

如果以支付系統(tǒng)的支付訂單的訂單號(hào)做為分片鍵時(shí):
- 支付引擎的內(nèi)部系統(tǒng)可以使用分片鍵查詢,會(huì)路由到具體的庫(kù)表當(dāng)中,沒(méi)有問(wèn)題
- 渠道、結(jié)算、記賬等系統(tǒng)如果涉及到回調(diào)支付引擎,在調(diào)用的時(shí)候會(huì)把支付引擎的支付單號(hào)傳遞給后續(xù)系統(tǒng),如果進(jìn)行回調(diào)操作時(shí)候,可以回傳這個(gè)支付單號(hào)。會(huì)路由到具體的庫(kù)表當(dāng)中,沒(méi)有問(wèn)題
- 收銀臺(tái)需要根據(jù)交易的支付訂單查詢支付引擎生成的支付單。由于不是根據(jù)分片鍵查詢,不能路由到具體的庫(kù)中的具體表上,會(huì)進(jìn)行全表掃描。就會(huì)有問(wèn)題。
3、解決方案
首先想到的方案可以參考收銀臺(tái)系統(tǒng),把收銀臺(tái)調(diào)用支付引擎看到外部調(diào)用。然后添加一張映射表,把收銀臺(tái)生成的支付流水號(hào)與支付引擎的支付單號(hào)關(guān)聯(lián)起來(lái)。當(dāng)收銀臺(tái)需要查詢支付引擎時(shí),可以先通過(guò)映射表查詢到具體的支付單號(hào),這樣就可以進(jìn)行分片鍵操作數(shù)據(jù)源了。這個(gè)方案存在一個(gè)問(wèn)題存在以下幾個(gè)問(wèn)題:
引入了關(guān)聯(lián)表,添加了系統(tǒng)復(fù)雜度進(jìn)行數(shù)據(jù)查詢的時(shí)候會(huì)兩次查詢,先查詢映射表,然后再查詢支付單
那么有沒(méi)有其它方案呢?答案是肯定的。
我們來(lái)看一下收銀臺(tái)、支付引擎其實(shí)這兩個(gè)系統(tǒng)在支付系統(tǒng)中是同一個(gè)緯度的。如果收銀臺(tái)的交易訂單進(jìn)行支付的時(shí)候,就會(huì)在支付引擎當(dāng)中下一筆支付單。我們可以把交易單與支付單在同一個(gè)水平緯度上進(jìn)行數(shù)據(jù)庫(kù)拆分。
什么叫同一個(gè)緯度的數(shù)據(jù)庫(kù)拆分呢?
其實(shí)就是收銀臺(tái)的支付訂單進(jìn)行分庫(kù)分表之后,這條數(shù)據(jù)落在數(shù)據(jù)庫(kù)里面的哪一個(gè)庫(kù),哪一張表就一定了。這個(gè)時(shí)候支付引擎就可以通過(guò)這個(gè)單號(hào)獲取到具體的庫(kù)表信息。這樣就可以把支付引擎生成的的訂單號(hào)帶個(gè)具體的庫(kù)表信息。然后在進(jìn)行分庫(kù)分表算法定義的時(shí)候根據(jù)支付引擎生成的訂單號(hào)中帶的庫(kù)表信息路由到具體的庫(kù)表中去了。就樣就會(huì)解決上面的問(wèn)題,不需要映射表。同時(shí)這種方案也會(huì)帶來(lái)以下的問(wèn)題:
- 數(shù)據(jù)上游與下游的分庫(kù)分表必須一致
- 數(shù)據(jù)在進(jìn)行再次擴(kuò)容會(huì)有其它問(wèn)題
經(jīng)過(guò)討論決定使用方案二。
4、代碼實(shí)現(xiàn)
下面通過(guò) Sharding jdbc 的復(fù)合分片簡(jiǎn)單的模擬代碼實(shí)現(xiàn)。數(shù)據(jù)庫(kù)、表準(zhǔn)備:
數(shù)據(jù)庫(kù):
- order_0
- order_1
每個(gè)數(shù)據(jù)庫(kù)的表:
tb_order_0
tb_order_1
tb_order_2
tb_order_3
tb_order_4
tb_order_5
tb_order_6
tb_order_7
# 邏輯表
create table tb_order
(
trade_master_no varchar(16),
pay_order_no varchar(16) ,
);
# 準(zhǔn)備數(shù)據(jù)
# 分庫(kù)分表規(guī)則是前一位代表庫(kù),后一位代表表,所以在 order_1.tb_order_1 中添加以下數(shù)據(jù)
insert into tb_order_1 values('11', '11'),
4.1 Sharding JDBC 配置
下面是針對(duì)訂單表的 sharding jdbc 的分庫(kù)分表配置,數(shù)據(jù)庫(kù)連接池使用 Hikari 。分片規(guī)則:前一位代表庫(kù),后一位代表表。使用交易主單號(hào)(trade_master_no) 和 支付單號(hào)(pay_order_no) 作為復(fù)合分片。當(dāng)查詢條件中只要包含一個(gè)查詢規(guī)則時(shí)就會(huì)路由到具體庫(kù)表中。
ComplexShardingJDBCConfig.java
@Configuration
public class ComplexShardingJDBCConfig {
@Bean
public DataSource getShardingDataSource(HikariCommonConfig commonConfig) throws SQLException {
ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
shardingRuleConfig.getTableRuleConfigs().add(getShardingMessageTableRuleConfiguration());
Map<String, DataSource> dataSourceMap = new HashMap<>();
dataSourceMap.put("order_0", createDataSource(datasourceOne(commonConfig)));
dataSourceMap.put("order_1", createDataSource(datasourceTwo(commonConfig)));
Properties properties = new Properties();
properties.setProperty(ShardingPropertiesConstant.SQL_SHOW.getKey(), "true");
return ShardingDataSourceFactory.createDataSource(dataSourceMap, shardingRuleConfig, properties);
}
private TableRuleConfiguration getShardingMessageTableRuleConfiguration() {
TableRuleConfiguration shardingMessageConfiguration = new TableRuleConfiguration("tb_order", "order_${0..1}.tb_order_${0..7}");
shardingMessageConfiguration.setDatabaseShardingStrategyConfig(messageDatasourceShardingStrategyConfig());
shardingMessageConfiguration.setTableShardingStrategyConfig(messageTableShardingStrategyConfig());
return shardingMessageConfiguration;
}
private ComplexShardingStrategyConfiguration messageDatasourceShardingStrategyConfig(){
return new ComplexShardingStrategyConfiguration("trade_master_no,pay_order_no", new OrderDatasourceComplexKeysShardingAlgorithm());
}
private ShardingStrategyConfiguration messageTableShardingStrategyConfig() {
return new ComplexShardingStrategyConfiguration("trade_master_no,pay_order_no", new OrderTableComplexKeysShardingAlgorithm());
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.ds1")
public HikariConfig datasourceOne(HikariCommonConfig commonConfig){
HikariConfig hikariConfig = new HikariConfig();
hikariConfig.setMinimumIdle(commonConfig.getMinimumIdle());
hikariConfig.setIdleTimeout(commonConfig.getIdleTimeout());
hikariConfig.setMaximumPoolSize(commonConfig.getMaximumPoolSize());
hikariConfig.setPoolName(commonConfig.getPoolName());
hikariConfig.setMaxLifetime(commonConfig.getMaxLifetime());
hikariConfig.setConnectionTimeout(commonConfig.getConnectionTimeout());
hikariConfig.setConnectionTestQuery(commonConfig.getConnectionTestQuery());
return hikariConfig;
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.ds2")
public HikariConfig datasourceTwo(HikariCommonConfig commonConfig){
HikariConfig hikariConfig = new HikariConfig();
hikariConfig.setMinimumIdle(commonConfig.getMinimumIdle());
hikariConfig.setIdleTimeout(commonConfig.getIdleTimeout());
hikariConfig.setMaximumPoolSize(commonConfig.getMaximumPoolSize());
hikariConfig.setPoolName(commonConfig.getPoolName());
hikariConfig.setMaxLifetime(commonConfig.getMaxLifetime());
hikariConfig.setConnectionTimeout(commonConfig.getConnectionTimeout());
hikariConfig.setConnectionTestQuery(commonConfig.getConnectionTestQuery());
return hikariConfig;
}
private HikariDataSource createDataSource(HikariConfig hikariConfig) {
HikariDataSource sharding = new HikariDataSource();
BeanUtils.copyProperties(hikariConfig, sharding);
return sharding;
}
}
數(shù)據(jù)庫(kù)分片規(guī)則:
public class OrderDatasourceComplexKeysShardingAlgorithm implements ComplexKeysShardingAlgorithm<String> {
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames, ComplexKeysShardingValue<String> shardingValue) {
Map<String, Collection<String>> columnNameAndShardingValuesMap = shardingValue.getColumnNameAndShardingValuesMap();
if(columnNameAndShardingValuesMap.containsKey("trade_master_no")){
Collection<String> tradeMasterNos = columnNameAndShardingValuesMap.get("trade_master_no");
String tradeMasterNo = tradeMasterNos.iterator().next();
String datasourceSuffix = tradeMasterNo.substring(0, 1);
for (String availableTargetName : availableTargetNames) {
if(availableTargetName.endsWith(datasourceSuffix)){
return Lists.newArrayList(availableTargetName);
}
}
}
if(columnNameAndShardingValuesMap.containsKey("pay_order_no")){
Collection<String> payOrderNos = columnNameAndShardingValuesMap.get("pay_order_no");
String payOrderNo = payOrderNos.iterator().next();
String datasourceSuffix = payOrderNo.substring(0, 1);
for (String availableTargetName : availableTargetNames) {
if(availableTargetName.endsWith(datasourceSuffix)){
return Lists.newArrayList(availableTargetName);
}
}
}
throw new UnsupportedOperationException();
}
}
數(shù)據(jù)庫(kù)中的表分片規(guī)則:
public class OrderTableComplexKeysShardingAlgorithm implements ComplexKeysShardingAlgorithm<String> {
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames, ComplexKeysShardingValue<String> shardingValue) {
Map<String, Collection<String>> columnNameAndShardingValuesMap = shardingValue.getColumnNameAndShardingValuesMap();
if(columnNameAndShardingValuesMap.containsKey("trade_master_no")){
Collection<String> tradeMasterNos = columnNameAndShardingValuesMap.get("trade_master_no");
String tradeMasterNo = tradeMasterNos.iterator().next();
String datasourceSuffix = tradeMasterNo.substring(1, 2);
for (String availableTargetName : availableTargetNames) {
if(availableTargetName.endsWith(datasourceSuffix)){
return Lists.newArrayList(availableTargetName);
}
}
}
if(columnNameAndShardingValuesMap.containsKey("pay_order_no")){
Collection<String> payOrderNos = columnNameAndShardingValuesMap.get("pay_order_no");
String payOrderNo = payOrderNos.iterator().next();
String datasourceSuffix = payOrderNo.substring(1, 2);
for (String availableTargetName : availableTargetNames) {
if(availableTargetName.endsWith(datasourceSuffix)){
return Lists.newArrayList(availableTargetName);
}
}
}
throw new UnsupportedOperationException();
}
}
4.2 數(shù)據(jù)源操作類(lèi)
這里使用 Mybatis 操作數(shù)據(jù)源,當(dāng)然使用其它 ORM 框架操作數(shù)據(jù)源 sharding jdbc 也是支持的。
public interface OrderMapper {
int countByExample(OrderExample example);
int deleteByExample(OrderExample example);
int insert(Order record);
int insertSelective(Order record);
List<Order> selectByExample(OrderExample example);
int updateByExampleSelective(@Param("record") Order record, @Param("example") OrderExample example);
int updateByExample(@Param("record") Order record, @Param("example") OrderExample example);
}
4.3 分片測(cè)試類(lèi)
通過(guò) Spring boot 定義一個(gè) Controller,使用 Order 對(duì)象查詢。即可以使用交易單號(hào)也可以使用支付單號(hào)查詢。
@Getter
@Setter
public class Order {
private String tradeMasterNo;
private String payOrderNo;
}
@RestController
@RequestMapping("order")
public class OrderController {
@Resource
private OrderDao orderDao;
@RequestMapping("query")
public Order query(@RequestBody Order order) {
Order orderInDB = orderDao.queryOrder(order);
return orderInDB;
}
}
4.4 測(cè)試結(jié)果
由于我們?cè)?sharing jdbc 配置當(dāng)中配置了數(shù)據(jù)庫(kù)查詢 SQL,我們只需要觀察是不是只打印了一條數(shù)據(jù)庫(kù)操作語(yǔ)句就可以判斷之前的結(jié)論是否正確。
通過(guò) Postman 使用交易單號(hào)查詢:

控制臺(tái)打印:

然后通過(guò) Postman 使用支付單號(hào)查詢:

控制臺(tái)打印:

它們查詢都是路由到具體的庫(kù)表當(dāng)中,說(shuō)明我們的方案是可以的。
參考文章:
到此這篇關(guān)于SpringBoot集成Sharding Jdbc使用復(fù)合分片的實(shí)踐的文章就介紹到這了,更多相關(guān)SpringBoot集成Sharding Jdbc復(fù)合分片內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- springboot整合shardingjdbc實(shí)現(xiàn)分庫(kù)分表最簡(jiǎn)單demo
- Java使用Sharding-JDBC分庫(kù)分表進(jìn)行操作
- SpringBoot整合sharding-jdbc實(shí)現(xiàn)自定義分庫(kù)分表的實(shí)踐
- SpringBoot整合sharding-jdbc實(shí)現(xiàn)分庫(kù)分表與讀寫(xiě)分離的示例
- 使用Sharding-JDBC對(duì)數(shù)據(jù)進(jìn)行分片處理詳解
- 基于sharding-jdbc的使用限制
- ShardingSphere jdbc集成多數(shù)據(jù)源的實(shí)現(xiàn)步驟
- Java ShardingJDBC實(shí)戰(zhàn)演練
相關(guān)文章
hutool實(shí)戰(zhàn):IoUtil 流操作工具類(lèi)(將內(nèi)容寫(xiě)到流中)
這篇文章主要介紹了Go語(yǔ)言的io.ioutil標(biāo)準(zhǔn)庫(kù)使用,是Golang入門(mén)學(xué)習(xí)中的基礎(chǔ)知識(shí),需要的朋友可以參考下,如果能給你帶來(lái)幫助,請(qǐng)多多關(guān)注腳本之家的其他內(nèi)容2021-06-06
Java繼承方法重寫(xiě)實(shí)現(xiàn)原理及解析
這篇文章主要介紹了Java繼承方法重寫(xiě)實(shí)現(xiàn)原理及解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-12-12
java?socket實(shí)現(xiàn)局域網(wǎng)聊天
這篇文章主要為大家詳細(xì)介紹了java?socket實(shí)現(xiàn)局域網(wǎng)聊天,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-05-05
Spring如何通過(guò)注解存儲(chǔ)和讀取對(duì)象詳解
在Spring中,要想更簡(jiǎn)單的存儲(chǔ)和讀取對(duì)象的核心是使用注解,這篇文章主要給大家介紹了關(guān)于Spring如何通過(guò)注解存儲(chǔ)和讀取對(duì)象的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-07-07
阿里的Easyexcel讀取Excel文件的方法(最新版本)
這篇文章主要介紹了阿里的Easyexcel讀取Excel文件(最新版本)的方法,本文通過(guò)示例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-12-12
SpringBoot基于Actuator遠(yuǎn)程關(guān)閉服務(wù)
這篇文章主要介紹了SpringBoot基于Actuator遠(yuǎn)程關(guān)閉服務(wù),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-11-11
java 反射 動(dòng)態(tài)調(diào)用不同類(lèi)的靜態(tài)方法(推薦)
下面小編就為大家?guī)?lái)一篇JAVA 反射 動(dòng)態(tài)調(diào)用不同類(lèi)的靜態(tài)方法(推薦)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-08-08

