说明用户表类似订单表,未来数亿甚至亿级规模的海量数据表,一般只是单表设计,为了前期快速上线项目,无需考虑分库分表。随着业务的发展,单表容量超过千万甚至亿级。这时候就要考虑分库分表的问题了。分库分表不宕机迁移应该是分库分表最基本的需求,毕竟互联网项目不可能挂出“今晚10:00~10:00”的广告牌。次日00,系统将停机维护”。想要?使用codis,笔者之前正好遇到过这个问题,借鉴了codis的一些思路,实现了不停机分库分表的迁移计划;codis不是本文的重点,这里只提一下使用codis的地方--rebalance:当迁移过程中发生数据访问时,Proxy会向Redis发送“SLOTSMGRTTAGSLOT”迁移命令强制key待迁移的客户端访问,然后处理客户端的请求。(SLOTSMGRTTAGSLOT是codis基于redis定制的。)分库分表明确了这个方案后,更容易理解不停的分库分表迁移。下面详细介绍一下笔者原先对installed_app表的实现方案;即用户安装过的APP信息表;1.分库列的确定分库分表的列分库肯定是最重要的环节,没有之一。分库列直接决定整个分库分表方案最终能否成功实施;选择一个合适的shardingcolumn基本上可以让这个表相关的大部分流量接口都通过这个shardingcolumn访问分库分表最常见的shardingcolumn是user_id,这里注意也选择了user_id;2、根据自身业务选择最合适分库分表方案的分库列后,需要确定分库列。数据库分表计划。作者采用主动迁移和被动迁移相结合的方案:主动迁移是一个独立的程序,遍历需要分库分表的installed_app表,分库分表后将数据迁移到目标表。被动迁移是指installed_app表相关的业务代码将数据迁移到分库分表后对应的表中。接下来将详细介绍这两种解决方案;2.1ActiveMigrationActivemigration是一个独立的插件式迁移程序。它的作用是遍历需要分库分表的installed_app表,将这里的数据复制到分库分表后的目标表中,由于主动迁移和被动迁移会一起运行,所以需要处理主动迁移与被动迁移的碰撞问题。作者主动迁移伪代码如下:publicvoidmigrate(){//查询当前表的***ID,判断是否迁移完成longmaxId=execute("selectmax(id)frominstalled_app");longtempMinId=0L;longstepSize=1000;longtempMaxId=0L;do{try{tempMaxId=tempMinId+stepSize;//根据InnoDB的索引特性,whereid>=?andid=#{tempMinId}andid<#{tempMaxId}";ListinstalledApps=executeSql(scanSql);Iteratoriterator=installedApps.iterator();while(iterator.hasNext()){InstalledAppinstalledApp=iterator.next();//helpGCiterator.remove();longuserId=installedApp.getUserId();Stringstatus=executeRedis("getMigrateStatus:${userId}");if("COMPLETED".equals(status)){//migrationfinish,nothingtodocontinue;}if("MIGRATING".equals(status)){//"被动迁移"migration,nothingtodocontinue;}//迁移前获取锁:setMigrateStatus:18MIGRATINGex3600nxStringresult=executeRedis("setMigrateStatus:${userId}MIGRATINGex86400nx");if("OK".equals(result)){//获取锁成功后,先查询这个用户所有安装的app【即迁移过程在用户ID维度迁移】Stringsql="select*frominstalled_appwhereuser_id=#{user_id}";ListuserInstalledApps=executeSql(sql);//Installedthis用户安装的所有app都迁移到分库分表后的表中(分库分表后可以通过user_id获取具体的表)shardingInsertSql(userInstalledApps);//迁移后是完成,修改缓存状态executeRedis("setexMigrateStatus:${userId}864000COMPLETED");}else{//如果没有获取到锁,说明被动迁移已经获取到了锁,那么迁移就可以移交了到被动迁移【这个概率很低】//也可以strengthenedhere逻辑,“被动迁移”过程不能持续很长时间,可以尝试循环多次获取状态判断迁移是否完成logger.info("Migrationconflict.userId={}",userId);}}if(tempMaxId>=maxId){//更新max(id),最后确认遍历是否完成maxId=execute("selectmax(id)frominstalled_app");}logger.info("Migrationprocessid={}",tempMaxId);}catch(Throwablee){//如果执行过程中出现异常(这个异常可能只有redis和mysql会抛出),然后退出,修复问题再迁移//并设置tempMinId的值到logger.info("迁移ionprocessid="+tempMaxId);log中记录一次的id***防止重复迁移System.exit(0);}tempMinId+=stepSize;}while(tempMaxId=?和id=?遍历limitn或者limitm,n,因为limit的性能一般,随着遍历更进一步而id>=?和身份证应转换为Iterator,每次迭代后应移除每个userId,否则可能导致GC异常甚至OOM;2.2被动迁移被动迁移是在installed_app表相关的正常业务逻辑之前插入迁移逻辑。以新用户安装的APP为例,伪代码如下://被动迁移方法是一个普通逻辑,所以前面`installed_app`表相关的业务逻辑都需要调用该方法;publicvoidmigratePassive(longuserId)throwsException{Stringstatus=executeRedis("getMigrateStatus:${userId}");if("COMPLETED".equals(status)){//用户数据已经迁移完毕,nothingtodologger.info("user'sinstalledappmigrationcompleted.user_id={}",userId);}elseif("MIGRATING".equals(status)){//"被动迁移"迁移,等待迁移完成;为了防止无限循环,你可以添加***等待时间逻辑do{Thread.sleep(10);status=executeRedis("getMigrateStatus:${userId}");}while("COMPLETED".equals(status));}else{//准备迁移Stringresult=executeRedis("setMigrateStatus:${userId}MIGRATINGex86400nx");if("OK".equals(result)){//成功获取锁后,设置这个查询一个用户所有安装的app【即迁移过程在用户ID维度进行迁移】Stringsql="select*frominstalled_appwhereuser_id=#{user_id}";ListuserInstalledApps=executeSql(sql);//All将此用户安装的app迁移到分库分表后的表(可以通过user_id获取分库分表后的具体表)shardingInsertSql(userInstalledApps);//迁移完成后,修改缓存状态executeRedis("setexMigrateStatus:${userId}864000COMPLETED");}else{//如果没有获取到锁,应该是其他地方先获取到了锁,正在迁移,可以尝试等到migrationiscompleted}}}//`installed_app`表相关业务--添加用户安装的APPpublicvoidaddInstalledApp(InstalledAppinstalledApp)throwsException{//先尝试被动迁移migratePassive(installedApp.getUserId());//插入用户安装的应用信息(installedApp)分库分表后进入目标表中shardingInsertSql(installedApp);}不管CRUD中的操作如何,先根据缓存中MigrateStatus:${userId}的值判断:如果值为COMPLETED,表示迁移已经完成,然后将请求转移到分库分表如果值为MIGRATING,表示正在进行迁移,可以等到值为COMPLETED,即即,迁移完成后,再将请求转移到分库分表后的表中进行处理;否则值为Empty,则尝试获取锁,然后进行数据迁移。迁移完成后,将缓存值更新为COMPLETED,最后将请求转移到分库分表后的表中进行处理;3.当方案完善后,所有数据迁移完成最后,CRUD操作还是会根据缓存中MigrateStatus:${userId}的值来判断。数据迁移完成后,此步骤是多余的。可以添加一个主开关。所有数据迁移完成后,通过类似TOPIC的方式发送该开关的值。所有服务收到TOPIC后都会在本地缓存switch。那么下一个服务的增删改查不需要根据缓存中MigrateStatus:${userId}的值来判断;4.遗留工作迁移完成后,主动迁移程序下线,被动迁移程序中的migrationPassive()调用全部去掉,部分第三方分库分表中间件可以集成,比如sharding-jdbc。可以参考sharding-jdbc集成实践总结回顾回顾本方案。最大的缺点是,如果遇到分片列(比如userId)的记录总数比较大,主动迁移正在进行,被动迁移和主动迁移发生冲突,那么被动迁移可能需要等待许久。但是根据DB的性能,批量插入1000条数据一般需要10ms,而且同一个shardingcolumn的记录分库分表后只属于一张表,不涉及跨表表。所以,只要在迁移前通过SQL统计,待迁移表中没有出现这种异常分片列,就可以放心迁移;笔者在迁移installed_app表时,用户最多只有200个APP,无需过多考虑碰撞性能问题;没有完美的解决方案,但总有适合自己的解决方案;如果你有几万条记录的shardingcolumn,你可以先把这些shardingcolumns缓存起来,晚上迁移程序上线,先迁移这些。缓存的分片列数据可以最大限度地减少这些用户的迁移程序体验。当然,你也可以使用你想出的更好的解决方案。