最近主要的工作重心是数据库的容量规划。
随着业务的逐渐增大,原有保存在单表的数据量也日益增强。数据库数据会随着业务的发展而不断增多,因此数据操作,如增删改查的开销也会越来越大。再加上物理服务器的资源有限(CPU、磁盘、内存、IO 等)。最终数据库所能承载的数据量、数据处理能力都将遭遇瓶颈。换句话说需要合理的数据库架构来存放不断增长的数据,这个就是分库分表的设计初衷。目的就是为了缓解数据库的压力,大限度提高数据操作的效率。
数据库分库分表中间件是采用的 apache sharding。
ShardingSphere是一套开源的分布式数据库中间件解决方案组成的生态圈,它由Sharding-JDBC、Sharding-Proxy和Sharding-Sidecar(计划中)这3款相互独立的产品组成。 他们均提供标准化的数据分片、分布式事务和数据库治理功能,可适用于如Java同构、异构语言、云原生等各种多样化的应用场景。
ShardingSphere定位为关系型数据库中间件,旨在充分合理地在分布式的场景下利用关系型数据库的计算和存储能力,而并非实现一个全新的关系型数据库。 它与NoSQL和NewSQL是并存而非互斥的关系。NoSQL和NewSQL作为新技术探索的前沿,放眼未来,拥抱变化,是非常值得推荐的。反之,也可以用另一种思路看待问题,放眼未来,关注不变的东西,进而抓住事物本质。 关系型数据库当今依然占有巨大市场,是各个公司核心业务的基石,未来也难于撼动,我们目前阶段更加关注在原有基础上的增量,而非颠覆。
ShardingSphere已经在2020年4月16日从Apache孵化器毕业,成为Apache顶级项目。
因为我们公司属于第三方支付平台,这个改造的点可以分为两类:提供给商户调用的对接系统(比如收银台),系统内部调用的系统(支付引擎)。
数据源使用分库分表,在 Sharding JDBC 当中如果进行 修改、删除、查询操作中没有包含分片键就会进行全表扫描。所以在进行业务改造的时候对原有的数据库操作都进行了业务优化,基本改造后的所有的操作都使用了基于分片键进行操作(定时任务除外)。
首先讨论一下,提供给商户调用的系统。在进行下单操作的时候,商户必须传递商户号和外部订单号。对于外部订单号第三方支付系统无法控制,只需要商户每次传递过来的时候与历史的外部订单号不重复就可以了。所以这里就涉及到一张映射表,这个表的主要功能如下:
这个时候对交易订单就依赖于外部映射表,把请求映射成内部订单号进行分片就可以了
当商户下好了交易订单的时候,需要进行支付这个时候就产生了一笔支付订单。交易订单和支付订单是一对多的关系。当用户进行支付的时候会调用支付引擎,这个时候正常情况下一般会生成支付系统的支付订单。然后支付引擎会调用后续的渠道、结算、记账等系统,系统之间的调用图如下:
首先想到的方案可以参考收银台系统,把收银台调用支付引擎看到外部调用。然后添加一张映射表,把收银台生成的支付流水号与支付引擎的支付单号关联起来。当收银台需要查询支付引擎时,可以先通过映射表查询到具体的支付单号,这样就可以进行分片键操作数据源了。这个方案存在一个问题存在以下几个问题:
那么有没有其它方案呢?答案是肯定的。
我们来看一下收银台、支付引擎其实这两个系统在支付系统中是同一个纬度的。如果收银台的交易订单进行支付的时候,就会在支付引擎当中下一笔支付单。我们可以把交易单与支付单在同一个水平纬度上进行数据库拆分。
什么叫同一个纬度的数据库拆分呢?
其实就是收银台的支付订单进行分库分表之后,这条数据落在数据库里面的哪一个库,哪一张表就一定了。这个时候支付引擎就可以通过这个单号获取到具体的库表信息。这样就可以把支付引擎生成的的订单号带个具体的库表信息。然后在进行分库分表算法定义的时候根据支付引擎生成的订单号中带的库表信息路由到具体的库表中去了。就样就会解决上面的问题,不需要映射表。同时这种方案也会带来以下的问题:
经过讨论决定使用方案二。
下面通过 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'),
下面是针对订单表的 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 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 {
@Override
public Collection doSharding(Collection availableTargetNames, ComplexKeysShardingValue shardingValue) {
Map> columnNameAndShardingValuesMap = shardingValue.getColumnNameAndShardingValuesMap();
if(columnNameAndShardingValuesMap.containsKey("trade_master_no")){
Collection 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 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 {
@Override
public Collection doSharding(Collection availableTargetNames, ComplexKeysShardingValue shardingValue) {
Map> columnNameAndShardingValuesMap = shardingValue.getColumnNameAndShardingValuesMap();
if(columnNameAndShardingValuesMap.containsKey("trade_master_no")){
Collection 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 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();
}
}
这里使用 Mybatis 操作数据源,当然使用其它 ORM 框架操作数据源 sharding jdbc 也是支持的。
public interface OrderMapper {
int countByExample(OrderExample example);
int deleteByExample(OrderExample example);
int insert(Order record);
int insertSelective(Order record);
List selectByExample(OrderExample example);
int updateByExampleSelective(@Param("record") Order record, @Param("example") OrderExample example);
int updateByExample(@Param("record") Order record, @Param("example") OrderExample example);
}
通过 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;
}
}
由于我们在 sharing jdbc 配置当中配置了数据库查询 SQL,我们只需要观察是不是只打印了一条数据库操作语句就可以判断之前的结论是否正确。
通过 Postman 使用交易单号查询:
参考文章: