导读
node
上一篇文章《ShardingJdbc分库分表实战案例解析(上)》中咱们初步介绍了使用ShardingJdbc实现订单数据分散存储的分库分表方法,在本篇文章中将重点介绍在不停服的状况下实现数据分片存储的在线扩容。具体将以以下两个常见的场景进行演示:1)、还没有进行分库分表的单库单表系统如何平稳的实施分库分表方案;2)、已经实施过度库分表方案的系统,因为数据量的持续增加致使原有分库分表不够用了,须要二次扩容的状况。mysql
实施方案概述
web
在具体演示以前,咱们先简单聊一聊关于分库分表数据迁移中,几种常见的思路,具体以下:算法
1)、停服迁移
spring
停服迁移方案,是数据迁移方案中最多见、也是相对安全,可是也是最不能让人接受的方案。之因此说它安全,是由于停服以后就不会有新的数据写入,这样能保证数据迁移工做可以在一个相对稳定的环境中进行,于是能较大程度上避免迁移过程当中产生数据不一致的问题。
sql
但停服务方案会比较严重伤害用户体验、下降服务的可用性,而若是数据量较大迁移须要大量的时间的话,长时间的停服会严重影响业务,对于强调"9999"服务体验的互联网公司来讲,停服方案绝对是让人不能接受的。
数据库
通常来讲停服方案更适合于哪些非7X24小时,且对自身数据一致性要求很是高的系统,例如社保基金、银行核心系统等。固然,这也并非说非停服方案,作不到数据迁移的绝对准确,仅仅在于这些系统出于管理、规则等非技术因素上的考虑是否可以容忍小几率的风险事件罢了。express
停服迁移的流程,通常以下:apache
一、提早进行演练、预估停服时间,发布停服公告;json
二、停服,经过事先准备好的数据迁移工具(通常为迁移脚本),按照新的数据分片规则,进行数据迁移;
三、核对迁移数据的准确性;
四、修改应用程序代码,切换数据分片读写规则,并完成测试验证;
五、启动服务,接入外部流量;
2)、升级从库方案
关于升级从库的方案,通常是针对线上数据库配置了主从同步结构的系统来讲的。其具体思路是,当须要从新进行分库分表扩容时,可将现有从库直接升级成主库。例如原先分库分表结构是A、B两个分库为主库、A0、B0分别为A、B对应的从库,具体以下图所示:
假设此时若是须要扩容,新增两个分库的话,那么能够将A0、B0升级为主库,如此原先的两个分库将变为4个分库。与此同时,将上层服务的分片规则进行更改,将原先uid%2=0(原先存在A库)的数据分裂为uid%4=0和uid%=2的数据分别存储在A和A0上;同时将uid%2=1(原先存在B库)的数据分裂为uid%4=1和uid%=3的数据分别存储在B和B0上。而由于A0库和A库,B0库与B1库数据相同,因此这样就不须要进行数据迁移了,只须要变动服务的分片规则配置便可。以后结构以下:
而以前uid%2的数据分配在2个库中,此时分散到4个库,因为老数据还在,因此uid%4=2的数据还有一半存储在uid%4=0的分库中。所以还须要对冗余数据进行清理,但这类清理并不影响线上数据的一致性,能够随时随地进行。
处理完成后,为保证高可用,以及下一步扩容的需求,能够为现有主库再次分配一个从库,具体以下图所示:
升级从库方案的流程,通常以下:
一、修改服务数据分片配置,作好新库和老库的数据映射;
二、DBA协助完成从库升级为主库的数据库切换;
三、DBA解除现有数据库主从配置关系;
四、异步完成冗余数据的清理;
五、从新为新的数据节点搭建新的从库;
这种方案避免了因为数据迁移致使的不肯定性,但也有其局限性。首先,现有数据库主从结构要能知足新的分库分表的规划;其次,这种方案的主要技术风险点被转移至DBA,实施起来可能会有较大阻力,毕竟DBA也不见得愿意背这个锅;最后,因为须要在线更改数据库的存储结构,可能也会出现意想不到的状况,而若是还存在多应用共享数据库实例状况的话,状况也会变得比较复杂。
3)、双写方案
双写方案是针对线上数据库迁移时使用的一种常见手段,而对于分库分表的扩容来讲,也涉及到数据迁移,因此也能够经过双写来协助分库分表扩容的问题。
双写方案实际上同升级从库的原理相似,都是作"分裂扩容",从而减小直接数据迁移的规模下降数据不一致的风险,只是数据同步的方式不一样。双写方案的核心步骤是:1)、是直接增长新的数据库,并在原有分片逻辑中增长写连接,同时写两份数据;2)、与此同时,经过工具(例如脚本、程序等)将原先老库中的历史数据逐步同步至新库,此时因为新库只有新增写入,应用上层其余逻辑还在老库之中,因此数据的迁移对其并没有影响;3)、对迁移数据进行校验,因为是业务直接双写,因此新增数据的一致性是很是高的(但须要注意insert、update、delete操做都须要双更新操做);4)、完成数据迁移同步,并校验一致后就能够在应用上层根据老库的分裂方式从新修改分片配置规则了。之前面的例子为例,双写方案以下图所示:
如上图所示原先的A、B两个分库,其中uid%2=0的存放在A库,uid%2=1的存放在B库;增长新的数据库,其中写入A库是双写A0库,写入B库时双写B0库。
以后分别将A库的历史数据迁移至A0库;B库的历史数据迁移至B0库。最终确保A库与A0库的数据一致;B库与B0库的数据一致。具体以下图所示:
以后修改分片规则,确保原先uid%2=0存放在A库的数据,在分裂为uid%4=0和uid%4=2的状况下能分别存储在A库和A0库中;原先uid%2=1存放在B库的数据,在分裂为uid%4=1和uid%4=3的状况下分别存在到B库和B0库之中。具体以下图所示:
双写方案避免了像升级从库那样改变数据库结构的风险,更容易由开发人员本身控制,但双写方案须要侵入应用代码,而且最终须要完成数据迁移和冗余数据删除两个步骤,实施起来也不轻松。
那么到底有没有一个绝对完美的方案呢?
答案实际上是没有!由于不管哪一种方案都没法避免要迁移数据,即使像升级从库那样避免了数据迁移,也没法避免对冗余数据进行删除的额外操做。但咱们能够在数据迁移手段和从新处理数据分片的方式上进行优化,目前在分库分表领域著名的开源项目ShardingSphere本质上其实就是在这两方面进行了优化,从而提供了一组解决方案工具集。
具体来讲ShardingSphere是由"Sharding-JDBC+Sharding-Proxy+Sharding-Scaling"三个核心组件组成的分库分表开源解决方案,Sharding-JDBC在《ShardingJdbc分库分表实战案例解析(上)》中咱们已经介绍过。而Sharding-Proxy+Sharding-Scaling则是专门用于设计处理分库分表扩容数据迁移问题的组件,其运行原理以下:
如上图所示,在ShardingSphere的解决方案中,当须要对Service(旧)服务进行分库分表扩容时,咱们能够先部署Sharding-Scaling+Sharding-Proxy组件进行数据迁移+数据分片预处理。具体来讲步骤以下:
1)、在Sharding-Proxy中按照扩容方案配置好分片规则,启动服务并提供JDBC协议链接机制,此时经过Sharding-Proxy链接写入的数据会按照新的分片规则进行数据存储;
2)、部署Sharding-Scaling服务,并经过HTTP接口的形式,向其发送数据迁移任务配置(配置数据中有须要迁移的数据库链接串,也有Sharding-Proxy的数据链接串);
3)、启动Sharding-Scaling迁移任务后,Sharding-Scaling将根据目标数据源的Binlog日志变化,读取后从新发送至Sharding-Proxy进行分片数据的从新处理;
4)、当Sharding-Scaling迁移数据任务完成,检查数据迁移结果,若是没有问题,则能够修改Service(新)服务的数据分片规则,并完成Service(旧)服务的替换;
5)、确认扩容无误后,中止Sharding-Proxy+Sharding-Scaling服务;
6)、异步完成冗余数据的清理(目前ShardingSphere还不支持数据迁移后自动完成冗余数据的清理,因此须要本身根据数据分裂规则,编写清除脚本);
上述过程彻底自动,在完成数据迁移及从新分片前,旧服务保持持续服务,不会对线上形成影响;此外因为Sharding-Scaling一直处于监听目标数据源Binlog日志状态,因此即使在服务切换过程当中,旧Service服务仍有数据写入,也会自动被Sharding-Proxy从新分片处理,因此不用担忧会出现不一致。
此外Sharding-Scaling+Sharding-Proxy通讯方式采用的是JDBC链接原生链接方式以及基于Binlog日志的同步方案,因此在迁移效率上也是有保证的。接下来将基于Sharding-Scaling+Sharding-Proxy方案具体演示在不停服的状况下进行分库分表在线扩容。
ShardingSphere分库分表在线扩容
仍是以上一篇文章中的订单分库分表存储为例,将其原有的分库分表规划:1)、数据库节点2个(ds0、ds1);2)、每一个库的分表数为32张表(0~31)。扩容为:1)、数据库节点4个(ds0、ds一、ds二、ds3);2)、每一个库的分表数仍为32张表(0~31)。
首先咱们部署Sharding-Scaling+Sharding-Proxy进行在线数据迁移及数据分片处理,具体以下:
1)、部署Sharding-Proxy
该服务的做用是一个数据库中间件,咱们在此服务上编辑好分库分表规则后,Sharding-Scaling会把原数据写入Sharding-Proxy,而后由Sharding-Proxy对数据进行路由后写入对应的库和表。
咱们能够在Github上下载ShardingSphere对应的版本的源码(演示所用版本为4.1.1)自行编译,也能够下载已经编译好的版本文件。本次演示所用方式为源码编译,其执行程序目录以下:
/shardingsphere-4.1.1/sharding-distribution/sharding-proxy-distribution/target/apache-shardingsphere-4.1.1-sharding-proxy-bin.tar.gz
找到编译执行程序后进行解压!以后编辑"conf/server.yaml"文件,添加链接帐号配置,具体以下:
authentication:
users:
root:
password: 123456
该配置主要是供Sharding-Scaling链接Sharding-Proxy时使用!以后编辑Sharding-Proxy的分库分表配置文件"conf/config-sharding.yaml ",按照新的扩容方案进行配置,具体以下:
#对外暴露的数据库名称
schemaName: order
dataSources:
ds_0:
url: jdbc:mysql://127.0.0.1:3306/order_0?serverTimezone=UTC&useSSL=false
username: root
password: 123456
connectionTimeoutMilliseconds: 30000
idleTimeoutMilliseconds: 60000
maxLifetimeMilliseconds: 1800000
maxPoolSize: 300
ds_1:
url: jdbc:mysql://127.0.0.1:3306/order_1?serverTimezone=UTC&useSSL=false
username: root
password: 123456
connectionTimeoutMilliseconds: 30000
idleTimeoutMilliseconds: 60000
maxLifetimeMilliseconds: 1800000
maxPoolSize: 300
ds_2:
url: jdbc:mysql://127.0.0.1:3306/order_2?serverTimezone=UTC&useSSL=false
username: root
password: 123456
connectionTimeoutMilliseconds: 30000
idleTimeoutMilliseconds: 60000
maxLifetimeMilliseconds: 1800000
maxPoolSize: 300
ds_3:
url: jdbc:mysql://127.0.0.1:3306/order_3?serverTimezone=UTC&useSSL=false
username: root
password: 123456
connectionTimeoutMilliseconds: 30000
idleTimeoutMilliseconds: 60000
maxLifetimeMilliseconds: 1800000
maxPoolSize: 300
shardingRule:
tables:
t_order:
actualDataNodes: ds_${0..3}.t_order_$->{0..31}
databaseStrategy:
inline:
shardingColumn: user_id
algorithmExpression: ds_${user_id % 4}
tableStrategy:
inline:
shardingColumn: order_id
algorithmExpression: t_order_${order_id % 32}
keyGenerator:
type: SNOWFLAKE
column: id
完成分库分表配置后,启动Sharding-Proxy服务,命令以下:
sh bin/start.sh
为了验证ShardingProxy的部署正确下,能够经过Mysql命令进行链接,并插入一条数据,验证其是否按照新的分库分表规则进行存储,具体以下:
#使用mysql客户端检查是否能连接成功
mysql -h 127.0.0.1 -p 3307 -uroot -p123456
mysql> show databases;
+----------+
| Database |
+----------+
| order |
+----------+
1 row in set (0.02 sec)
#执行如下脚本,写入成功后, 检查order_0~3的分表数据是否按照规则落库
mysql> insert into t_order values('d8d5e92550ba49d08467597a5263205b',10001,'topup',100,'CNY','2','3','1010010101',63631722,now(),now(),'测试sharding-proxy');
Query OK, 1 row affected (0.20 sec)
按照新的分库分表规则uid->63631722%4=2;orderId->10001%32=17,测试数据应该落在ds_2中的t_order_17表中!如正确插入,则说明新的分库分表规则配置正确。
2)、部署Sharding-Scaling
源码编译的Sharding-Scaling执行程序包路径为:
/shardingsphere-4.1.1/sharding-distribution/sharding-scaling-distribution/target/apache-shardingsphere-4.1.1-sharding-scaling-bin.tar.gz
解压后,启动Scaling服务,命令以下:
sh bin/start.sh
Sharding-Scaling是一个独立的数据迁移服务,其自己不与任何具体的环境关联,在建立迁移任务时,具体信息由接口传入。接下来咱们调用Sharding-Scaling建立具体的迁移任务。
在此以前,原有分库中的数据有:
1)、userId=63631725;orderId=123458存储在ds_1中的t_order_2号表中;
2)、userId=63631722;orderId=123457存储在ds_0中的t_order_1号表中;
而按照新的规则数据1不须要迁移,数据2须要迁移至ds_2中的t_order_1号表中,具体建立Sharding-Scaling迁移任务的指令以下:
#提交order_0的迁移数据命令
curl -X POST --url http://localhost:8888/shardingscaling/job/start \
--header 'content-type: application/json' \
--data '{
"ruleConfiguration": {
"sourceDatasource": "ds_0: !!org.apache.shardingsphere.orchestration.core.configuration.YamlDataSourceConfiguration\n dataSourceClassName: com.zaxxer.hikari.HikariDataSource\n properties:\n jdbcUrl: jdbc:mysql://127.0.0.1:3306/order_0?serverTimezone=UTC&useSSL=false&zeroDateTimeBehavior=convertToNull\n driverClassName: com.mysql.jdbc.Driver\n username: root\n password: 123456\n connectionTimeout: 30000\n idleTimeout: 60000\n maxLifetime: 1800000\n maxPoolSize: 100\n minPoolSize: 10\n maintenanceIntervalMilliseconds: 30000\n readOnly: false\n",
"sourceRule": "tables:\n t_order:\n actualDataNodes: ds_0.t_order_$->{0..31}\n keyGenerator:\n column: order_id\n type: SNOWFLAKE",
"destinationDataSources": {
"name": "dt_1",
"password": "123456",
"url": "jdbc:mysql://127.0.0.1:3307/order?serverTimezone=UTC&useSSL=false",
"username": "root"
}
},
"jobConfiguration": {
"concurrency": 1
}
}'
以上咱们提交了针对老库order_0的数据迁移任务,若是提交成功Sharding-Scaling会返回以下信息:
{"success":true,"errorCode":0,"errorMsg":null,"model":null}
此时可经过命令查看任务详情进度,命令及结果显示以下:
#curl http://localhost:8888/shardingscaling/job/progress/1;
{
"success": true,
"errorCode": 0,
"errorMsg": null,
"model": {
"id": 1,
"jobName": "Local Sharding Scaling Job",
"status": "RUNNING",
"syncTaskProgress": [{
"id": "127.0.0.1-3306-order_0",
"status": "SYNCHRONIZE_REALTIME_DATA",
"historySyncTaskProgress": [{
"id": "history-order_0-t_order_24#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_25#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_22#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_23#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_20#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_21#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_19#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_17#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_18#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_15#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_16#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_13#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_14#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_8#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_11#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_9#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_12#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_6#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_31#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_7#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_10#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_4#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_5#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_30#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_2#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_3#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_0#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_1#0",
"estimatedRows": 1,
"syncedRows": 1
}, {
"id": "history-order_0-t_order_28#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_29#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_26#0",
"estimatedRows": 0,
"syncedRows": 0
}, {
"id": "history-order_0-t_order_27#0",
"estimatedRows": 0,
"syncedRows": 0
}],
"realTimeSyncTaskProgress": {
"id": "realtime-order_0",
"delayMillisecond": 8759,
"logPosition": {
"filename": "mysql-bin.000001",
"position": 190285,
"serverId": 0
}
}
}]
}
}
一样针对其余老库,如order_1数据库的迁移,也能够以相似的方式提交迁移任务!若是此时观察数据2,就会发现其已经被从新分片到ds_2的t_order_1号表中;但其以前的历史分片数据仍然会冗余在ds_0的t_order_1号表中(须要清理)。
假设此时,有一条经过老服务写入的"userId=63631723&orderId=123457"数据,那么其会被存储在ds_1的t_order_1号表中,而其最新存储规则应该在ds_3的t_order_1号表中。若是以前也同时开启了order_1库的Scaling数据迁移任务,那么此时该数据将会被自动从新迁移并分片至ds_3的t_order_1号表中。
完成数据迁移后,就能够将以前旧服务的分片规则进行调整并从新发布了,具体以下:
#SQL控制台打印(开发时配置)
spring.shardingsphere.props.sql.show = true
# 配置真实数据源
spring.shardingsphere.datasource.names=ds0,ds1,ds2,ds3
# 配置第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
# 配置第3个数据源
spring.shardingsphere.datasource.ds2.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.ds2.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds2.url=jdbc:mysql://127.0.0.1:3306/order_2?characterEncoding=utf-8
spring.shardingsphere.datasource.ds2.username=root
spring.shardingsphere.datasource.ds2.password=123456
# 配置第4个数据源
spring.shardingsphere.datasource.ds3.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.ds3.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds3.url=jdbc:mysql://127.0.0.1:3306/order_3?characterEncoding=utf-8
spring.shardingsphere.datasource.ds3.username=root
spring.shardingsphere.datasource.ds3.password=123456
# 配置t_order表规则
spring.shardingsphere.sharding.tables.t_order.actual-data-nodes=ds$->{0..3}.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表
# ...
以上内容详细介绍并演示了针对已经分库分表的系统进行二次扩容时使用Sharding-Scaling+Sharding-Proxy进行数据迁移的过程;而关于还没有进行过度库分表,但须要进行分库分表的系统来讲,其过程与二次扩容并没有差异,这里就再也不赘述!
分库分表实践中须要注意的其余问题
在具体的分库分表实践中还须要注意表的主键问题,通常能够考虑分布式ID生成方案,例如UUID等,避免在扩容迁移数据时发生主键冲突。另外关于Sharding-Scaling+Sharding-Proxy的使用,也须要注意一些异常状况,目前Sharding-Scaling的版本仍是Alpha版本,因此使用过程当中不排除会有一些问题,能够多看看源码,增进了解!
最后因为篇幅的缘由,就没有具体演示历史数据的清理方法,你们若是在实践中有更好的方法,也欢迎同步给我!以上就是本篇文章想要表达的所有内容了,但愿对你们有用!
—————END—————
参考资料:
https://shardingsphere.apache.org/document/current/en/quick-start/shardingsphere-scaling-quick-start/
https://blog.csdn.net/beichen8641/article/details/106924189
本文分享自微信公众号 - 无敌码农(jiangqiaodege)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。