SpringBoot集成Sharding Jdbc使用复合分片的实践

 更新时间:2021年9月26日 16:01  点击:2045

最近主要的工作重心是数据库的容量规划。

随着业务的逐渐增大,原有保存在单表的数据量也日益增强。数据库数据会随着业务的发展而不断增多,因此数据操作,如增删改查的开销也会越来越大。再加上物理服务器的资源有限(CPU、磁盘、内存、IO 等)。最终数据库所能承载的数据量、数据处理能力都将遭遇瓶颈。换句话说需要合理的数据库架构来存放不断增长的数据,这个就是分库分表的设计初衷。目的就是为了缓解数据库的压力,大限度提高数据操作的效率。

数据库分库分表中间件是采用的 apache sharding。

1、Sharing JDBC 简介

ShardingSphere是一套开源的分布式数据库中间件解决方案组成的生态圈,它由Sharding-JDBC、Sharding-Proxy和Sharding-Sidecar(计划中)这3款相互独立的产品组成。 他们均提供标准化的数据分片、分布式事务和数据库治理功能,可适用于如Java同构、异构语言、云原生等各种多样化的应用场景。

ShardingSphere定位为关系型数据库中间件,旨在充分合理地在分布式的场景下利用关系型数据库的计算和存储能力,而并非实现一个全新的关系型数据库。 它与NoSQL和NewSQL是并存而非互斥的关系。NoSQL和NewSQL作为新技术探索的前沿,放眼未来,拥抱变化,是非常值得推荐的。反之,也可以用另一种思路看待问题,放眼未来,关注不变的东西,进而抓住事物本质。 关系型数据库当今依然占有巨大市场,是各个公司核心业务的基石,未来也难于撼动,我们目前阶段更加关注在原有基础上的增量,而非颠覆。

ShardingSphere已经在2020年4月16日从Apache孵化器毕业,成为Apache顶级项目。

2、系统改造

因为我们公司属于第三方支付平台,这个改造的点可以分为两类:提供给商户调用的对接系统(比如收银台),系统内部调用的系统(支付引擎)。

  • 收银台系统:核心功能是提供给商户提交交易订单,并且对这笔交易订单进行支付的支付订单
  • 支付引擎:接收这个支付产品的请求,调用渠道,记账,结算等功能

数据源使用分库分表,在 Sharding JDBC 当中如果进行 修改、删除、查询操作中没有包含分片键就会进行全表扫描。所以在进行业务改造的时候对原有的数据库操作都进行了业务优化,基本改造后的所有的操作都使用了基于分片键进行操作(定时任务除外)。

2.1 对接外部系统的系统

首先讨论一下,提供给商户调用的系统。在进行下单操作的时候,商户必须传递商户号和外部订单号。对于外部订单号第三方支付系统无法控制,只需要商户每次传递过来的时候与历史的外部订单号不重复就可以了。所以这里就涉及到一张映射表,这个表的主要功能如下:

  • 把商户的外部订单号映射成内部订单号
  • 通过商户号与商户的外部订单号在数据库联合唯一达到幂等处理
  • 保存商户请求的原始数据,做为请求凭证

这个时候对交易订单就依赖于外部映射表,把请求映射成内部订单号进行分片就可以了

2.2 内部系统间的调用

当商户下好了交易订单的时候,需要进行支付这个时候就产生了一笔支付订单。交易订单和支付订单是一对多的关系。当用户进行支付的时候会调用支付引擎,这个时候正常情况下一般会生成支付系统的支付订单。然后支付引擎会调用后续的渠道、结算、记账等系统,系统之间的调用图如下:

在这里插入图片描述

如果以支付系统的支付订单的订单号做为分片键时:

  • 支付引擎的内部系统可以使用分片键查询,会路由到具体的库表当中,没有问题
  • 渠道、结算、记账等系统如果涉及到回调支付引擎,在调用的时候会把支付引擎的支付单号传递给后续系统,如果进行回调操作时候,可以回传这个支付单号。会路由到具体的库表当中,没有问题
  • 收银台需要根据交易的支付订单查询支付引擎生成的支付单。由于不是根据分片键查询,不能路由到具体的库中的具体表上,会进行全表扫描。就会有问题。

3、解决方案

首先想到的方案可以参考收银台系统,把收银台调用支付引擎看到外部调用。然后添加一张映射表,把收银台生成的支付流水号与支付引擎的支付单号关联起来。当收银台需要查询支付引擎时,可以先通过映射表查询到具体的支付单号,这样就可以进行分片键操作数据源了。这个方案存在一个问题存在以下几个问题:

引入了关联表,添加了系统复杂度进行数据查询的时候会两次查询,先查询映射表,然后再查询支付单

那么有没有其它方案呢?答案是肯定的。

我们来看一下收银台、支付引擎其实这两个系统在支付系统中是同一个纬度的。如果收银台的交易订单进行支付的时候,就会在支付引擎当中下一笔支付单。我们可以把交易单与支付单在同一个水平纬度上进行数据库拆分。

什么叫同一个纬度的数据库拆分呢?

其实就是收银台的支付订单进行分库分表之后,这条数据落在数据库里面的哪一个库,哪一张表就一定了。这个时候支付引擎就可以通过这个单号获取到具体的库表信息。这样就可以把支付引擎生成的的订单号带个具体的库表信息。然后在进行分库分表算法定义的时候根据支付引擎生成的订单号中带的库表信息路由到具体的库表中去了。就样就会解决上面的问题,不需要映射表。同时这种方案也会带来以下的问题:

  • 数据上游与下游的分库分表必须一致
  • 数据在进行再次扩容会有其它问题

经过讨论决定使用方案二。

4、代码实现

下面通过 Sharding jdbc 的复合分片简单的模拟代码实现。数据库、表准备:

数据库:
- order_0
- order_1

每个数据库的表:
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) ,
);

# 准备数据
# 分库分表规则是前一位代表库,后一位代表表,所以在 order_1.tb_order_1 中添加以下数据
insert into tb_order_1 values('11', '11'),

4.1 Sharding JDBC 配置

下面是针对订单表的 sharding jdbc 的分库分表配置,数据库连接池使用 Hikari 。分片规则:前一位代表库,后一位代表表。使用交易主单号(trade_master_no) 和 支付单号(pay_order_no) 作为复合分片。当查询条件中只要包含一个查询规则时就会路由到具体库表中。

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;
	}

}

数据库分片规则:

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();
	}

}

数据库中的表分片规则:

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 数据源操作类

这里使用 Mybatis 操作数据源,当然使用其它 ORM 框架操作数据源 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 分片测试类

通过 Spring boot 定义一个 Controller,使用 Order 对象查询。即可以使用交易单号也可以使用支付单号查询。

@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 测试结果

由于我们在 sharing jdbc 配置当中配置了数据库查询 SQL,我们只需要观察是不是只打印了一条数据库操作语句就可以判断之前的结论是否正确。

通过 Postman 使用交易单号查询:

在这里插入图片描述

控制台打印:

在这里插入图片描述

然后通过 Postman 使用支付单号查询:

在这里插入图片描述

控制台打印:

在这里插入图片描述

它们查询都是路由到具体的库表当中,说明我们的方案是可以的。

参考文章:

Sharding-JDBC 官网

到此这篇关于SpringBoot集成Sharding Jdbc使用复合分片的实践的文章就介绍到这了,更多相关SpringBoot集成Sharding Jdbc复合分片内容请搜索猪先飞以前的文章或继续浏览下面的相关文章希望大家以后多多支持猪先飞!

[!--infotagslink--]

相关文章

  • 解决springboot使用logback日志出现LOG_PATH_IS_UNDEFINED文件夹的问题

    这篇文章主要介绍了解决springboot使用logback日志出现LOG_PATH_IS_UNDEFINED文件夹的问题,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-04-28
  • SpringBoot实现excel文件生成和下载

    这篇文章主要为大家详细介绍了SpringBoot实现excel文件生成和下载,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2021-02-09
  • SpringBoot集成Redis实现消息队列的方法

    这篇文章主要介绍了SpringBoot集成Redis实现消息队列的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-02-10
  • 详解springBoot启动时找不到或无法加载主类解决办法

    这篇文章主要介绍了详解springBoot启动时找不到或无法加载主类解决办法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-09-16
  • 解决Springboot get请求是参数过长的情况

    这篇文章主要介绍了解决Springboot get请求是参数过长的情况,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-09-17
  • Springboot+TCP监听服务器搭建过程图解

    这篇文章主要介绍了Springboot+TCP监听服务器搭建过程,本文通过图文并茂的形式给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2020-10-28
  • Spring Boot项目@RestController使用重定向redirect方式

    这篇文章主要介绍了Spring Boot项目@RestController使用重定向redirect方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教...2021-09-02
  • springBoot 项目排除数据库启动方式

    这篇文章主要介绍了springBoot 项目排除数据库启动方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教...2021-09-10
  • springboot中使用@Transactional注解事物不生效的坑

    这篇文章主要介绍了springboot中使用@Transactional注解事物不生效的原因,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-01-26
  • 详解SpringBoot之访问静态资源(webapp...)

    这篇文章主要介绍了详解SpringBoot之访问静态资源(webapp...),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-09-14
  • SpringBoot接口接收json参数解析

    这篇文章主要介绍了SpringBoot接口接收json参数解析,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教...2021-10-19
  • Springboot mybatis plus druid多数据源解决方案 dynamic-datasource的使用详解

    这篇文章主要介绍了Springboot mybatis plus druid多数据源解决方案 dynamic-datasource的使用,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2020-11-18
  • ShardingSphere jdbc集成多数据源的实现步骤

    本文主要介绍了ShardingSphere jdbc集成多数据源的实现步骤,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2021-10-21
  • springboot多模块包扫描问题的解决方法

    这篇文章主要介绍了springboot多模块包扫描问题的解决方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-09-16
  • Springboot+MDC+traceId日志中打印唯一traceId

    本文主要介绍了Springboot+MDC+traceId日志中打印唯一traceId,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2021-10-17
  • Springboot实现多线程注入bean的工具类操作

    这篇文章主要介绍了Springboot实现多线程注入bean的工具类操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-08-27
  • SpringBoot部署到Linux读取resources下的文件及遇到的坑

    本文主要给大家介绍SpringBoot部署到Linux读取resources下的文件,在平时业务开发过程中,很多朋友在获取到文件内容乱码或者文件读取不到的问题,今天给大家分享小编遇到的坑及处理方案,感兴趣的朋友跟随小编一起看看吧...2021-06-21
  • 关于springboot中nacos动态路由的配置

    这篇文章主要介绍了springboot中nacos动态路由的配置方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教...2021-09-11
  • Java连接MySQL8.0 JDBC的详细步骤(IDEA版本)

    这篇文章主要介绍了Java连接MySQL8.0 JDBC的详细步骤(IDEA版本),本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2021-04-06
  • 解决Springboot整合shiro时静态资源被拦截的问题

    这篇文章主要介绍了解决Springboot整合shiro时静态资源被拦截的问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-01-26