分表分库实现思路
技术选型这一难题解决后,具体如何落实分表分库方案呢?需要考虑5个要点。
1)使用什么字段作为分片主键?
2)分片的策略是什么?
3)业务代码如何修改?
4)历史数据如何迁移?
5)未来的扩容方案是什么?
具体如下。
使用什么字段作为分片主键
先来回顾一下业务场景中的数据库示例,见表3-4。
表3-4 用户和订单数据量
把表3-4中的数据拆分成一个订单表,表中主要数据结构见表3-5。
表3-5 订单主要数据结构
表t_order使用user_ID作为分片主键,为什么呢?当时的思路如下。
在选择分片主键之前,首先要了解系统中的一些常见业务需求。
1)用户需要查询所有订单,订单数据中肯定包含不同的user_ID、order_time。
2)后台需要根据城市查询当地的订单。
3)后台需要统计每个时间段的订单趋势。
根据这些常见业务需求,判断一下优先级,用户操作(也就是第一个需求)必须优先满足。
此时如果使用user_ID作为订单的分片主键,就能保证每次用户查询数据(第一个需求)时,在一个分库的一个分表里即可获取数据。
因此,在方案里,最终还是使用user_ID作为分片主键,这样在分表分库查询时,首先会把user_ID作为参数传过来。
分片的策略是什么
决定使用user_ID作为订单分片主键后,就要开始考虑使用何种分片策略了。
目前通用的分片策略分为根据范围分片、根据Hash值分片、根据Hash值及范围混合分片这3种。
1)根据范围分片:比如user_ID是自增型数字,把user_ID按照每100万份分为一个库,每10万份分为一个表的形式进行分片,见表3-6。
表3-6 范围分片表结构
说明:这里只讲分表,分库就是把分表分组存放在一个库即可。
2)根据Hash值分片:指的是根据user_ID的Hash值mod(取模)一个特定的数进行分片(为了方便后续扩展,一般是2n)。
3)根据Hash值及范围混合分片:先按照范围分片,再根据Hash值取模分片。比如,表名=order_#user_ID% 10#_#hash(user_ID)%8,即分成了10×8=80个表,如图3-4所示。
以上3种分片策略到底应该选择哪个?只需要考虑一点:假设之后数据量变大了,需要把表分得更细,此时保证迁移的数据尽量少即可。
因此,根据Hash值分片时,一般建议拆分成2n个表。比如分成8张表,数据迁移时把原来的每张表拆一半出来组成新表,这样数据迁移量就小了。
当初的方案中,就是根据user_ID的Hash值按32取模,把数据分到32个数据库中,每个数据库再分成16张表。
简 单 计 算 一 下 , 假 设 每 天 订 单 量 为 1000 万 , 则 每 个 库 日 增 1000万/16=31.25万,每个表日增1000万/32/16=1.95万,3年后每个表的数据量就是2000万左右,仍在可控范围内。
• 图3-4 Hash值和范围混合分片结构
如果业务增长特别快,且运维还能承受,为避免以后出现扩容问题,建议库分得越多越好。
业务代码如何修改
分片策略确定后,就要考虑业务代码如何修改了。因业务代码修改与业务强关联,所以该项目采用的方案不具备通用性,这里就没有列出来。
但是,笔者在这里分享一些经验。近年来,分表分库操作更加容易,不过需要注意几个要点。
1)如果使用微服务,对于特定表的分表分库,其影响面只为该表所在的服务,而如果是一个单体架构的应用做分表分库,那会很麻烦。因为单体架构里面会有很多的跨表关联查询,也就是说,很多地方会直接与订单表一起进行Join查询,这种情况下,要想将订单数据拆分到多个库、多个表中,修改的代码就会非常多。
2)在互联网架构中,基本不使用外键约束。
3)分库分表以后,与订单有关的一些读操作都要考虑对应的数据是在哪个库哪个表。可以的话,尽量避免跨库或跨表查询。
一般来说,除了业务代码需要修改以外,历史数据的迁移也是一个难点。
历史数据如何迁移
历史数据的迁移非常耗时,迁移几天几夜都很正常。而在互联网行业中,别说几天几夜,就算停机几分钟,业务都可能无法接受,这就要求给出一个无缝迁移的解决方案。
讲解查询分离时提过一个方案,就是监控数据库变更日志,将数据库变更的事件变成消息,存到消息系统,然后有个消费者订阅消息,再将变动的数据同步到查询数据库,如图3-5所示。
• 图3-5 监控数据库日志更新查询数据示意图
历史数据迁移就可以采用类似的方案,如图3-6所示。
• 图3-6 分表分库数据迁移方案示意图
此数据迁移方案的基本思路为:旧架构继续运行,存量数据直接迁移,增量数据监听binlog,然后通过canal通知迁移程序迁移数据,等到新的数据库拥有全量数据且校验通过后再逐步切换流量到新架构。
数据迁移解决方案的详细步骤如下。
1)上线canal,通过canal触发增量数据的迁移。
2)迁移数据脚本测试通过后,将老数据迁移到新的分表分库中。
3)注意迁移增量数据与迁移老数据的时间差,确保全部数据都被迁移过去,无任何遗漏。
4)此时新的分表分库中已经拥有全量数据了,可以运行数据验证程序,确保所有数据都存放在新数据库中。
到这里数据迁移就算完成了,之后就是新版本代码上线,至于是灰度上线还是直接上线,需要根据实际情况决定,回滚方案也是一样。
未来的扩容方案是什么
随着业务的发展,如果原来的分片设计已经无法满足日益增长的数据量的需求,就需要考虑扩容了。扩容方案主要依赖以下两点。
1)分片策略是否可以让新表数据的迁移源只有一个旧表,而不是多个旧表?这就是前面建议使用2n分表的原因——以后每次扩容都能扩为2倍,都是把原来一张表的数据拆分到两张表中。
2)数据迁移。需要把旧分片的数据迁移到新的分片上,这个方案与上面提及的历史数据迁移一样,此处不再赘述。
小结
分表分库的解决方案就讲完了,这也是业界常用的做法。这个方案实现以后,项目组对它做了一些压力测试,1亿订单量的情况下,基本上也能做到20毫秒之内响应。
后来,随着业务的发展,在分表分库系统上线的11个月后,日订单量达到了100万。事实证明,在大数据时代,提前考虑大数据量的到来是必要的。
不过系统在营销高峰期还是出了问题:宕机1小时。但问题不在订单数据库这边,而是出现在一个商品API服务的缓存上。订单数据库和商品API服务分别由订单组和商品组负责。
回到这个方案,它在订单读写层面基本是足够的,至少保证了数据库不会宕机,不会因为订单量大系统就撑不住。
不过该方案还有一些不足之处。
1)复杂查询慢:很多查询需要跨订单数据库进行,然后再组合结果集,这样的查询比较慢。业界的普遍做法是前面提到的查询分离。查询分离那一片文章讲了单独使用Elasticsearch做查询分离的方案,这里分表分库的二期项目也进行了查询分离,只是查询数据存到了Elasticsearch和HBase中。Elasticsearch存放订单ID、用来查询关键字的字段以及查询页面列表里用到的字段,HBase存放订单的全量数据。Elasticsearch先根据用户的查询组合返回查询结果到查询页面。用户点击特定的订单,就能根据订单ID去HBase获取订单的全量数据。
2)增量数据迁移的高可用性和一致性:如果是自己编写迁移的代码,那就参考前面冷热分离和查询分离的迁移逻辑;也可以使用开源工具,这个方案在后面数据同步的场景中会单独展开。
3)短时订单量大爆发:分表分库可以解决数据量大的问题,但是如果瞬时流量非常大,数据库撑不住怎么办?这一问题会在后面的缓存和秒杀架构等场景中专门展开。
至此,数据持久化层的所有场景就介绍完了。之后将进入缓存层场景实战。