在对诸如订单、交易、支付等实时在线业务系统的研发、维护过程中,随着业务量的快速增长,我们经常会遇到由于关系型数据库(如:MySql)单表数据量增长过大而引发的线上事故;虽然这些事故多数时候是由于不合理的慢SQL而引起的系统雪崩,但有时也会出现由于数据库热点块IO争用而引发的系统性性能下降。总之,单表数据量的无限增长总是会在这样或那样的情况下增加系统的不稳定性因素。
所以在大规模实时系统的设计中,除了重点考虑应用结构的分布式化外,往往也不应该忽略数据库实时存储、计算能力扩展性方面的考虑。目前解决实时数据增长一般有两种思路:一种是直接采用分布式数据库(例如:Tidb、OceanBase之类);另一种是对关系型数据库进行分库分表来最大化利用现有数据库的实时计算能力。绝大部分情况下,后一种方案往往会更现实一些。
本文的主要内容就是通过模拟一个交易系统的订单库,来具体演示如何通过ShardingJdbc实现交易订单数据的分库分表存储。在这个过程中会到涉及分库分表实践的三种主要场景:1、新系统在设计之初直接使用分库分表方案;2、历史系统运行一段时间后如何平滑地实施分库分表;3、对现有分库分表逻辑的Scaling操作(包括减少分表、增加分表)涉及的数据迁移问题。
Spring Boot集成ShardingJdbc实现分库分表
交易系统的订单数据是分库分表的一个非常典型场景,由于交易系统对单条数据的实时处理性能要求很高,所以一旦单个订单表数据量规模达到10亿+,就很容易出现由于数据库热点块IO争用而导致的性能下降,也很容易出现个别不谨慎的SQL操作而引起的系统性雪崩。
但一旦决定实施分库分表就要提前做好存储规划,并对未来数据增长的规模进行一定的评估,同时做好未来增加分库、增加分表的系统Scaling方案。此外,分库分表的实施还要考虑应用的接入难度,分库分表的细节逻辑应该对应用透明;所以一般来说我们需要一个中间代理层来屏蔽分库分表对应用程序本身带来的侵入。
目前在Java社区中比较知名的分库分表代理组件就是ShardingJdbc(目前已被集成在Apache开源项目 ShardingSphere之中),ShardingJdbc本质上是一个轻量级的JDBC驱动代理,在使用的过程中只需要依赖相关Jar包即可,并不需要额外部署任何服务。通过系统配置文件就可以实现分库分表逻辑的定义,并实现应用透明代理访问。
接下来,我们以Spring Boot为例演示如何集成ShardingJdbc实现对交易订单的分库分表操作,具体步骤如下:
1)、订单数据的分库分表规划
在系统设计之初,如果能够预见到未来数据量的增长规模,那么提前做好分库分表规划是非常有远见的。从分库分表的形式上来说,一般可以有两类规划方式:1)、单库水平分表,如果单一数据库计算能力比较强,可以在同一个库中进行数据表的水平拆分;2)、分库+分表,如果数据规模爆炸式增长,单库的计算资源有限,为了提升数据库的整体计算处理性能,也可以同时实现多个库的分库分表存储。
在本文的实例中,我们将订单数据分库分表规划为:1)、数据库节点2个(ds0、ds1);2)、每个库的分表数为32张表(0~31)。订单表的整体数据分库分表逻辑是根据订单表中的“user_id字段%2”实现分库;然后在分库逻辑的基础上根据订单表中的“order_id字段%32”实现水平分表。例如,有条user_id为1001、订单编号为20200713001的订单数据,根据上述分库分表规则1001%2=1,20200713001%32=9,那么该数据将存储在ds1库中的第9个分表。
具体的订单逻辑表结构如下:
create table t_order (
id bigint not null primary key auto_increment,
order_id bigint comment '业务方订单号(业务方系统唯一)',
trade_type varchar (30) comment '业务交易类型,例如topup-表示钱包充值',
amount bigint comment '交易金额,以分为单位',
currency varchar (10) comment '币种,cny-人民币',
status varchar (2) comment '支付状态,0-待支付;1-支付中;2-支付成功;3-支付失败',
channel varchar (10) comment '支付渠道编码,0-微信支付,1-支付宝支付',
trade_no varchar (32) comment '支付渠道流水号',
user_id bigint (60) comment '业务方用户id',
update_time timestamp null default current_timestamp on update current_timestamp comment '最后一次更新时间',
create_time timestamp null default current_timestamp comment '交易创建时间',
remark varchar(128) comment '订单备注信息',
key unique_idx_pay_id ( order_id ),
key idx_user_id ( user_id ),
key idx_create_time ( create_time )
);
alter table t_order comment '交易订单表';
以上逻辑表的具体分表形式为t_order_{0~31},分别分布在ds0、ds1两个数据库节点中。
2)、创建实验工程代码结构
首先创建一个基于Maven构建的Spring Boot项目,并集成MyBatis数据库访问框架,代码结构如下:
如上图所示,我们创建了一个基于Spring Boot的基本工程,并在集成了基于Mybatis的数据库访问功能。此外该工程还实现了单元/集成测试代码的分离管理,具体可参考:Java如何优雅地实现单元测试与集成测试。
3)、SpringBoot+ShardingJdbc实现订单分库分表规则配置
接下来我们来看下在Spring Boot项目中如何集成ShardingJdbc,并按照规划的分库分表规则进行具体的配置。
首先引入ShardingJdbc针对Spring Boot项目的starter依赖包,具体如下:
org.apache.shardingsphere
sharding-jdbc-spring-boot-starter
${sharding-sphere.version}
org.apache.shardingsphere
sharding-jdbc-spring-namespace
${sharding-sphere.version}
引入Spring Boot Starter依赖后,ShardingJdbc会使用自己的数据源配置逻辑,为避免冲突需要在主类中排除掉默认的数据源自动配置类,具体如下:
//排除掉默认的数据源自动配置类
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class OrderServerApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServerApplication.class, args);
}
}
完成上述操作后,从工程逻辑上看就已经完成了ShardingJdbc与Spring Boot应用的集成。接下来我们要做的就是根据规划的分库分表规则,通过配置文件进行分库分表规则的配置,具体如下:
#SQL控制台打印(开发时配置)
spring.shardingsphere.props.sql.show = true
# 配置真实数据源
spring.shardingsphere.datasource.names=ds0,ds1
# 配置第1个数据源
spring.shardingsphere.datasource.ds0.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.ds0.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds0.url=jdbc:mysql://127.0.0.1:3306/order_0?characterEncoding=utf-8
spring.shardingsphere.datasource.ds0.username=root
spring.shardingsphere.datasource.ds0.password=123456
# 配置第2个数据源
spring.shardingsphere.datasource.ds1.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.ds1.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds1.url=jdbc:mysql://127.0.0.1:3306/order_1?characterEncoding=utf-8
spring.shardingsphere.datasource.ds1.username=root
spring.shardingsphere.datasource.ds1.password=123456
# 配置t_order表规则
spring.shardingsphere.sharding.tables.t_order.actual-data-nodes=ds$->{0..1}.t_order_$->{0..31}
# 配置t_order表分库策略(inline-基于行表达式的分片算法)
spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.sharding-column=user_id
spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.algorithm-expression=ds${user_id % 2}
# 配置t_order表分表策略
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 % 32}
#如其他表有分库分表需求,配置同上述t_order表
# ...
上述配置文件中,我们配置了两个数据源,对应的数据库分别为order_0、order_1,这两个数据库中分别存储了t_order这张逻辑表的分表信息{0~31},总共64张数据库来分散存储订单信息。分库分表的维度主要有2个分别是:用户ID作为分库键、订单ID作为分表键。
4)、编写订单入库逻辑测试ShardingJdbc分库分表效果
通过上述步骤,到这里我们已经从功能上完成了针对订单表的分库分表逻辑。具体针对订单表的操作逻辑,还是和正常使用Mybatis操作数据库表一样,并不需要针对分库分表进行额外的代码操作,因为ShardingJdbc会在数据库驱动层拦截SQL并进行分库分表规则的匹配及路由操作。
和正常基于Spring Mvc的开发一样,我们编写一个基于Mvc分层的订单创建接口,启动应用程序,效果如下:
可以看到从编程方式上看,与我们平时写Java代码的分层结构完全一致,此时模拟调用该订单创建接口,具体请求参数如下:
{
"orderId":123458,
"tradeType":"topup",
"amount":1000,
"currency":"cny",
"userId":63631725
}
按照该请求参数的数据规则,此条订单应该存在编号为1数据库中编号2的分表中,具体计算(“userId->63631725%2=1;orderId->123458%32=2”),完成接口调用后可以查询数据库表数据进行验证!
前面我们演示了在预先规划好分库分表结构的情况下,使用ShardingJdbc实现了应用透明的分库分表操作。但大部分情况下,能够有先见之明提前规划好长远数据表分散存储方案的系统是很少的,只有当数据规模达到一定的级别,系统性能遇到相应瓶颈时分库分表方案才会被顺理成章的放到桌面选项中。
而这一般又会涉及两个场景的场景:1)、尚未进行分库分表的单库单表系统如何平稳的实施分库分表方案;2)、已经实时过分库分表方案的系统,由于数据量的持续增长导致原有分库分表不够用了,需要二次扩容的情况。
上述两个场景,无论哪种情况都会面临由于存储规则改变而需要大量进行数据迁移的情况,这对于正在线上运行的系统来说,无异于是要给正在高速行驶中的汽车换轮子,稍有不慎就会造成系统崩溃的严重后果。所以敢于对这类系统进行结构性重塑的程序员都是真的猛士!
因此在分库分表方案中如何平稳地完成数据迁移、保证服务的持续可用是设计分库分表方案时需要充分考虑的核心问题。由于文章篇幅的关系,我将在下一篇文章《ShardingJdbc分库分表实战案例解析(下)》中详细介绍处理这个关键问题的具体方法和手段,敬请关注!
—————END—————