热点技术讲解:ShardingJdbc分库分表实战案例解析(上)

在对诸如订单、交易、支付等实时在线业务系统的研发、维护过程中,随着业务量的快速增长,我们经常会遇到由于关系型数据库(如: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的数据库访问功能。此外该工程还实现了单元/集成测试代码的分离管理。

3)、SpringBoot+ShardingJdbc实现订单分库分表规则配置

接下来我们来看下在Spring Boot项目中如何集成ShardingJdbc,并按照规划的分库分表规则进行具体的配置。

首先引入ShardingJdbc针对Spring Boot项目的starter依赖包,具体如下:

<!-- 引入Sharding-JDBC Spring Boot依赖组件 -->
<!-- Sharding-JDBC For Spring Boot Start -->
<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
    <version>${sharding-sphere.version}</version>
</dependency>
<!-- for spring namespace -->
<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>sharding-jdbc-spring-namespace</artifactId>
    <version>${sharding-sphere.version}</version>
</dependency>
<!-- Sharding-JDBC For Spring Boot End -->

引入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)、已经实时过分库分表方案的系统,由于数据量的持续增长导致原有分库分表不够用了,需要二次扩容的情况。*

上述两个场景,无论哪种情况都会面临由于存储规则改变而需要大量进行数据迁移的情况,这对于正在线上运行的系统来说,无异于是要给正在高速行驶中的汽车换轮子,稍有不慎就会造成系统崩溃的严重后果。所以敢于对这类系统进行结构性重塑的程序员都是真的猛士!

写在最后

欢迎大家关注我的公众号【风平浪静如码】,海量Java相关文章,学习资料都会在里面更新,整理的资料也会放在里面。

觉得写的还不错的就点个赞,加个关注呗!点关注,不迷路,持续更新!!!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342