转自:不停机分库分表迁移,作者:阿飞Javaer
需求说明
借鉴codis
分库分表
明白这个方案后,了解不停机分库分表迁移就比较容易了,接下来详细介绍笔者当初对installed_app
表的实施方案;即用户已安装的APP信息表;
1. 确定sharding column
sharding column
绝对是分库分表最最最重要的环节,没有之一。sharding column直接决定整个分库分表方案最终是否能成功落地;一个合适的sharding column的选取,基本上能让与这个表相关的绝大部分流量接口都能通过这个sharding column访问分库分表后的单表,而不需要跨库跨表,最常见的sharding column就是user_id
,笔记这里选取的也是user_id
;2. 分库分表方案
根据自身的业务选取最合适的sharding column后,就要确定分库分表方案了。笔者采用主动迁移
与被动迁移
相结合的方案:
-
主动迁移就是一个独立程序,遍历需要分库分表的
installed_app
表,将数据迁移到分库分表后的目标表中。 -
被动迁移就是与
installed_app
表相关的业务代码自身将数据迁移到分库分表后对应的表中。
接下来详细介绍这两个方案;
2.1 主动迁移
主动迁移就是一个独立的外挂迁移程序,其作用是遍历需要分库分表的installed_app
表,将这里的数据复制到分库分表后的目标表中,由于主动迁移
和被动迁移
会一起运行,所以需要处理主动迁移和被动迁移碰撞的问题,笔者的主动迁移
伪代码如下:
public void migrate(){ // 查询出当前表的最大ID, 用于判断是否迁移完成 long maxId = execute("select max(id) from installed_app"); long tempMinId = 0L; long stepSize = 1000; long tempMaxId = 0L; do{ try { tempMaxId = tempMinId + stepSize; // 根据InnoDB索引特性, where id>=? and id<?这种SQL性能最高 String scanSql = "select * from installed_app where id>=#{tempMinId} and id<#{tempMaxId}"; List<InstalledApp> installedApps = executeSql(scanSql); Iterator<InstalledApp> iterator = installedApps.iterator(); while (iterator.hasNext()) { InstalledApp installedApp = iterator.next(); // help GC iterator.remove(); long userId = installedApp.getUserId(); String status = executeRedis("get MigrateStatus:${userId}"); if ("COMPLETED".equals(status)) { // migration finish, nothing to do continue; } if ("MIGRATING".equals(status)) { // "被动迁移" migrating, nothing to do continue; } // 迁移前先获取锁: set MigrateStatus:18 MIGRATING ex 3600 nx String result = executeRedis("set MigrateStatus:${userId} MIGRATING ex 86400 nx"); if ("OK".equals(result)) { // 成功获取锁后, 先将这个用户所有已安装的app查询出来[即迁移过程以用户ID维度进行迁移] String sql = "select * from installed_app where user_id=#{user_id}"; List<InstalledApp> userInstalledApps = executeSql(sql); // 将这个用户所有已安装的app迁移到分库分表后的表中(有user_id就能得到分库分表后的具体的表) shardingInsertSql(userInstalledApps); // 迁移完成后, 修改缓存状态 executeRedis("setex MigrateStatus:${userId} 864000 COMPLETED"); } else { // 如果没有获取到锁, 说明被动迁移已经拿到了锁, 那么迁移交给被动迁移即可[这种概率很低] // 也可以加强这里的逻辑, "被动迁移"过程不可能持续很长时间, 可以尝试循环几次获取状态判断是否迁移完 logger.info("Migration conflict. userId = {}", userId); } } if (tempMaxId >= maxId) { // 更新max(id),因为迁移过程中由于双写,导致max(id)会有变化,所以需要再次确认maxId的值判断是否遍历完成 maxId = execute("select max(id) from installed_app"); } logger.info("Migration process id = {}", tempMaxId); }catch (Throwable e){ // 如果执行过程中有任何异常(这种异常只可能是redis和mysql抛出来的), 那么退出, 修复问题后再迁移 // 并且将tempMinId的值置为logger.info("Migration process id="+tempMaxId);日志最后一次记录的id, 防止重复迁移 System.exit(0); } tempMinId += stepSize; }while (tempMaxId < maxId); }
这里有几点需要注意:
- 第一步查询出max(id)是为了尽量减少max(id)的查询次数,假如第一次查询max(id)为10000000,那么直到遍历的id到10000000以前,都不需要再次查询max(id);
-
根据
id>=? and id<?
遍历,而不要根据id>=? limit n
或者limit m, n
进行遍历,因为limit性能一般,且会随着遍历越往后,性能越差。而id>=? and id<?
这种遍历方式即使会有一些踩空,也没有任何影响,且整个性能曲线非常平顺,不会有任何抖动;迁移程序毕竟是辅助程序,不能对业务程序有过多的影响; - 根据id区间范围查询出来的
List<InstalledApp>
要转换为Iterator<InstalledApp>
,每迭代处理完一个userId,要remove掉,否则可能导致GC异常,甚至OOM;
2.2 被动迁移
被动迁移就是在正常与installed_app
表相关的业务逻辑前插入了迁移逻辑,以新增用户已安装APP为例,其伪代码如下:
// 被动迁移方法是公用逻辑,所以与`installed_app`表相关的业务逻辑前都需要调用这个方法; public void migratePassive(long userId)throws Exception{ String status = executeRedis("get MigrateStatus:${userId}"); if ("COMPLETED".equals(status)) { // 该用户数据已经迁移完成, nothing to do logger.info("user's installed app migration completed. user_id = {}", userId); }else if ("MIGRATING".equals(status)) { // "被动迁移" migrating, 等待直到迁移完成; 为了防止死循环, 可以增加最大等待时间逻辑 do{ Thread.sleep(10); status = executeRedis("get MigrateStatus:${userId}"); }while ("COMPLETED".equals(status)); }else { // 准备迁移 String result = executeRedis("set MigrateStatus:${userId} MIGRATING ex 86400 nx"); if ("OK".equals(result)) { // 成功获取锁后, 先将这个用户所有已安装的app查询出来[即迁移过程以用户ID维度进行迁移] String sql = "select * from installed_app where user_id=#{user_id}"; List<InstalledApp> userInstalledApps = executeSql(sql); // 将这个用户所有已安装的app迁移到分库分表后的表中(有user_id就能得到分库分表后的具体的表) shardingInsertSql(userInstalledApps); // 迁移完成后, 修改缓存状态 executeRedis("setex MigrateStatus:${userId} 864000 COMPLETED"); }else { // 如果没有获取到锁, 应该是其他地方先获取到了锁并正在迁移, 可以尝试等待, 直到迁移完成 } } } // 与`installed_app`表相关的业务--新增用户已安装的APP public void addInstalledApp(InstalledApp installedApp) throws Exception{ // 先尝试被动迁移 migratePassive(installedApp.getUserId()); // 将用户已安装app信息(installedApp)插入到分库分表后的目标表中 shardingInsertSql(installedApp); // 单库单表的插入逻辑。是否需要这段旧业务代码,取决于方案的严谨性:如果需要方案可以回滚,那么这段代码需要保留; insertSql(installedApp); }
无论是CRUD中哪种操作,先根据缓存中MigrateStatus:${userId}
的值进行判断:
- 如果值为COMPLETED,表示已经迁移完成,那么将请求转移到分库分表后的表中进行处理即可;
- 如果值为MIGRATING,表示正在迁移中,可以循环等待直到值为COMPLETED即迁移完成后,再将请求转移到分库分表后的表中进行处理处理;
- 否则值为空,那么尝试获取锁再进行数据迁移。迁移完成后,将缓存值更新为COMPLETED,最后再将请求转移到分库分表后的表中进行处理处理;
3.方案完善1
MigrateStatus:${userId}
的值进行判断,数据迁移完成后这一步已经是多余的。可以加个总开关,当所有数据迁移完成后,将这个开关的值通过类似TOPIC的方式发送,所有服务接收到TOPIC后将开关local cache化。那么接下来服务的CRUD都不需要先根据缓存中MigrateStatus:${userId}
的值进行判断;4.方案完善2
installed_app
表的binlog(参考alibaba canal)来进一步优化,示例代码如下:// 与`installed_app`表相关的业务--新增用户已安装的APP--这段旧业务代码保持不变 public void addInstalledApp(InstalledApp installedApp) throws Exception{ insertSql(installedApp); }
// 当执行了新增SQL(insertSql(installedApp))后,会产生binlog日志,insert类型(canal可通过EventType判断)的binlog日志消费端的逻辑如下所示--即将被动迁移逻辑挪到binlog消费端处理即可: public void insertBinlogConsumer(InstalledApp installedApp){ // 先尝试被动迁移 migratePassive(installedApp.getUserId()); // 将用户已安装app信息(installedApp)插入到分库分表后的目标表中 shardingInsertSql(installedApp); }
5.遗留工作
迁移完成后,将主动迁移
程序下线,并将被动迁移
程序中对migratePassive()
的调用全部去掉,并可以集成一些第三方分库分表中间件,例如sharding-jdbc
,可以参考sharding-jdbc集成
回顾总结
回顾这个方案,最大的缺点就是如果碰到sharding column(例如userId)的总记录数比较多,且主动迁移正在进行中,被动迁移与主动迁移碰撞,那么被动迁移可能需要等待较长时间(如果采用binlog的方案,就没有这个缺点)。
不过根据DB性能,一般批量插入1000条数据都是10ms级别,并且同一sharding column的记录分库分表后只属于一张表,不涉及跨表。所以,只要在迁移前先通过sql统计待迁移表中没有这类异常sharding column即可放心迁移;
笔者当初迁移installed_app
表时,用户最多也只拥有不超过200个APP,所以不需要过多考虑碰撞带来的性能问题;没有万能的方案,但是有适合自己的方案;
如果有那种上千条记录的sharding column,可以把这些sharding column先缓存起来,迁移程序在夜间上线,优先迁移这些缓存的sharding column的数据,就可以尽可能的降低迁移程序对这些用户的体验。当然你也可以使用你想出来的更好的方案。
相关推荐
第三,**长时间停机分库分表**(01_长时间停机分库分表.png)可能是对传统迁移方法的对比,这种方法通常需要在维护窗口内完成,会有一段时间的服务中断。在实际操作中,这可能包括数据备份、结构迁移、数据导入等...
本文探讨了一种创新的分库分表扩容方式,旨在避免数据迁移,以实现更加平滑且低风险的scale-out扩容。这种方式特别适用于那些数据随着时间逐渐增长的应用场景。 传统的分库分表策略通常采用mod运算或基于时间的分片...
本文将主要探讨两个关键的优化技术:分库分表和不停机修改表结构。 **分库分表** 当单个表的数据量过大时,查询效率会显著下降,此时,分库分表成为一种必要的优化手段。分库指的是将数据分散到多个不同的数据库中...
**分库分表**是应对大数据量场景下的关键策略,用于提升数据库系统的读写性能和可扩展性。在MySQL中实现分库分表有多种方法,包括垂直分割和水平分割。 1. **垂直分割**:这种方法基于数据属性进行切分,将表中的列...
Java编程语言在面试中常常涉及的关键知识点包括方法的重载(Overloading)和重写(Overriding)、多态性、Synchronized关键字的理解以及数据库的分库分表策略。下面将详细阐述这些概念。 1. **方法重载(Overloading)**...
48_你们当时是如何把系统不停机迁移到分库分表的? 49_好啊!那如何设计可以动态扩容缩容的分库分表方案? 50_一个关键的问题!分库分表之后全局id咋生成? 51_说说MySQL读写分离的原理?主从同步延时咋解决? 52_...
DRDS将传统的单机数据库扩展到分布式环境,提供了分库分表、读写分离、平滑扩容等特性,适用于高并发、大数据量的互联网应用。 在开发指南中,主要涉及以下几个核心知识点: 1. **分库分表**:DRDS的核心功能之一...
DRDS是一种分布式数据库中间件,它通过分库分表、读写分离、分布式事务等特性,帮助用户实现对传统单机数据库的水平扩展,以应对大数据量、高并发的业务场景。 **1. 分布式数据库设计** DRDS基于分布式数据库理论,...
- **定义**: JCluster 是京东云针对大规模分布式环境设计的一套云数据库解决方案,其核心特性包括云数据库、数据库代理层(即JCluster本身)、分库分表能力、在线数据迁移以及简单的事务支持等。 - **架构组成**: -...
总结,阿里云的分布式数据库服务DRDS是应对大数据时代挑战的重要工具,它通过分库分表、读写分离和分布式事务等机制,为高并发、大数据量的业务提供稳定、高效的解决方案。深入学习和掌握DRDS,将有助于企业在数字化...
垂直拆分是根据业务功能将数据分散到不同的数据库中,而水平拆分则是根据数据特性进行切分,例如DRDS(分布式关系型数据库服务)提供的分库分表策略。然而,这两种方式都伴随着DBA(数据库管理员)工作量的增加和...
MYCAT是一款开源的分布式数据库中间件,主要用于解决大数据量下的高性能读写问题,它通过分库分表的方式,实现数据库的水平扩展。MYCAT的核心设计理念是“Scale Out”,即通过增加硬件设备的数量来提升系统处理能力...
"mysql分库分表"是Mycat的核心功能之一,它支持将大型的MySQL数据库横向扩展为多个子数据库,以此实现数据的分布式存储,有效解决了单一数据库在大数据量下的性能瓶颈问题。 Mycat作为一款开源的数据库中间件,其...
1. **分库分表**:通过分库分表技术,Mycat可以将大数据量的单表分成多个小表,分散到多个数据库服务器上,从而提高查询速度和系统负载能力。 2. **数据路由**:根据用户定义的规则,Mycat会自动选择合适的数据节点...
- DRDS通过分库分表策略,将传统的关系型数据库拆分为多个小库小表,从而分散负载,提高查询和写入性能。 - 它支持自动的数据迁移和负载均衡,使得数据库可以根据业务需求进行弹性扩展。 2. **高可用性**: - ...
LinuxONE的开放性使得技能复用和应用迁移变得简单,避免了平台锁定,同时,其强大的纵向扩展能力解决了分库分表带来的困扰,使银行能够专注于业务创新。 【应对互联网金融冲击】面对互联网金融的挑战,银行需要构建...
1. **分库分表**:Mycat支持水平扩展,通过将大数据量的单表分成多个小表,分散到不同的数据库服务器上,从而实现负载均衡,提升系统的并发处理能力。同时,Mycat提供了一种无缝的数据分片策略,确保数据的完整性。 ...