文档参考:书名:《从程序员到架构师:大数据量、缓存、高并发、微服务、多团队协同等核心场景实战》-王伟杰
前文如下:
1. 基于MySQL的分表分库
分表是将一份大的表数据进行拆分后存放至多个结构一样的拆分表中;分库就是将一个大的数据库拆分成类似于多个结构的小数据库。场景介绍里就举了个简单的例子,这里不再赘述。
1.1 需求
如果使用分表分库,有3个通用技术需求需要实现。
1)SQL组合:因为关联的表名是动态的,所以需要根据逻辑组装动态的SQL。比如,要根据一个订单的ID获取订单的相关数据,Select语句应该针对(From)哪一张表?
2)数据库路由:因为数据库名也是动态的,所以需要通过不同的逻辑使用不同的数据库。比如,如果要根据订单ID获取数据,怎么知道要连接哪一个数据库?
3)执行结果合并:有些需求需要通过多个分库执行后再合并归集起来。假设需要查询的数据分布在多个数据库的多个表中(比如在order1里面的t_order_1,order2里面的t_order_9中),那么需要将针对这些表的查询结果合并成一个数据集。
1.2 实现模式
而目前能解决以上问题的中间件分为两类:Proxy模式、Client模式。
1)Proxy模式:图3-2所示为ShardingSphere官方文档中的Proxy模式图,重点看中间的Sharding-Proxy层。这种设计模式将SQL组合、数据库路由、执行结果合并等功能全部放在了一个代理服务中,而与分表分库相关的处理逻辑全部放在了其他服务中,其优点是对业务代码无侵入,业务只需要关注自身业务逻辑即可。
2)Client模式:ShardingSphere官方文档中的Client模式如图3-3所示。这种设计模式将分表分库相关逻辑放在客户端,一般客户端的应用会引用一个jar,然后在jar中处理SQL组合、数据库路由、执行结果合并等相关功能。
这两种模式的中间件见表3-2。
这两种开源中间件的设计模式该如何选择呢?先简单对比一下它们的优缺点,见表3-3。
因为看重“代码灵活可控”这个优势,最终选择了Client模式里的Sharding-JDBC来实现分表分库。
2.分表分库实现思路
技术选型这一难题解决后,具体如何落实分表分库方案呢?需要考虑5个要点。
1)使用什么字段作为分片主键?
2)分片的策略是什么?
3)业务代码如何修改?
4)历史数据如何迁移?
5)未来的扩容方案是什么?
2.1 使用什么字段作为分片主键
先来回顾一下业务场景中的数据库示例,见表3-4。
表t_order使用user_ID作为分片主键,为什么呢?当时的思路如下。
在选择分片主键之前,首先要了解系统中的一些常见业务需求。
1)用户需要查询所有订单,订单数据中肯定包含不同的user_ID、order_time。
2)后台需要根据城市查询当地的订单。
3)后台需要统计每个时间段的订单趋势。根据这些常见业务需求,判断一下优先级,用户操作(也就是第一个需求)必须优先满足。此时如果使用user_ID作为订单的分片主键,就能保证每次用户查询数据(第一个需求)时,在一个分库的一个分表里即可获取数据。因此,在方案里,最终还是使用user_ID作为分片主键,这样在分表分库查询时,首先会把user_ID作为参数传过来。
2.2 分片的策略是什么
决定使用user_ID作为订单分片主键后,就要开始考虑使用何种分片策略了. 目前通用的分片策略分为根据范围分片、根据Hash值分片、根据Hash值及范围混合分片这3种。
1)根据范围分片:比如user_ID是自增型数字,把user_ID按照每100万份分为一个库,每10万份分为一个表的形式进行分片,见表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所示。
当初的方案中,就是根据user_ID的Hash值按32取模,把数据分到32个数据库中,每个数据库再分成16张表。简单计算一下,假设每天订单量为1000万,则每个库日增1000万/16=31.25万,每个表日增1000万/32/16=1.95万,3年后每个表的数据量就是2000万左右,仍在可控范围内。
2.3 业务代码如何修改
分片策略确定后,就要考虑业务代码如何修改了。因业务代码修改与业务强关联,所以该项目采用的方案不具备通用性,这里就没有列出来。
1)如果使用微服务,对于特定表的分表分库,其影响面只为该表所在的服务,而如果是一个单体架构的应用做分表分库,那会很麻烦。因为单体架构里面会有很多的跨表关联查询,也就是说,很多地方会直接与订单表一起进行Join查询,这种情况下,要想将订单数据拆分到多个库、多个表中,修改的代码就会非常多。
2)在互联网架构中,基本不使用外键约束。
3)分库分表以后,与订单有关的一些读操作都要考虑对应的数据是在哪个库哪个表。可以的话,尽量避免跨库或跨表查询。
4)一般来说,除了业务代码需要修改以外,历史数据的迁移也是一个难点。
2.4 历史数据如何迁移
历史数据的迁移非常耗时,迁移几天几夜都很正常。而在互联网行业中,别说几天几夜,就算停机几分钟,业务都可能无法接受,这就要求给出一个无缝迁移的解决方案。
讲解查询分离时提过一个方案,就是监控数据库变更日志,将数据库变更的事件变成消息,存到消息系统,然后有个消费者订阅消息,再将变动的数据同步到查询数据库,如图3-5所示。
历史数据迁移就可以采用类似的方案,如图3-6所示。
此数据迁移方案的基本思路为:旧架构继续运行,存量数据直接迁移,增量数据监听binlog,然后通过canal通知迁移程序迁移数据,等到新的数据库拥有全量数据且校验通过后再逐步切换流量到新架构。
数据迁移解决方案的详细步骤如下。
1)上线canal,通过canal触发增量数据的迁移。
2)迁移数据脚本测试通过后,将老数据迁移到新的分表分库中。
3)注意迁移增量数据与迁移老数据的时间差,确保全部数据都被迁移过去,无任何遗漏。
4)此时新的分表分库中已经拥有全量数据了,可以运行数据验证程序,确保所有数据都存放在新数据库中。到这里数据迁移就算完成了,之后就是新版本代码上线,至于是灰度上线还是直接上线,需要根据实际情况决定,回滚方案也是一样。
2.5 未来的扩容方案是什么
随着业务的发展,如果原来的分片设计已经无法满足日益增长的数据量的需求,就需要考虑扩容了。扩容方案主要依赖以下两点。
1)分片策略是否可以让新表数据的迁移源只有一个旧表,而不是多个旧表?这就是前面建议使用2n分表的原因——以后每次扩容都能扩为2倍,都是把原来一张表的数据拆分到两张表中。
2)数据迁移。需要把旧分片的数据迁移到新的分片上,这个方案与上面提及的历史数据迁移一样,此处不再赘述。
2.6 该方案一些不足之处。
1)复杂查询慢:很多查询需要跨订单数据库进行,然后再组合结果集,这样的查询比较慢。业界的普遍做法是前面提到的查询分离。第2章讲了单独使用Elasticsearch做查询分离的方案,这里分表分库的二期项目也进行了查询分离,只是查询数据存到了Elasticsearch和HBase中。Elasticsearch存放订单ID、用来查询关键字的字段以及查询页面列表里用到的字段,HBase存放订单的全量数据。 Elasticsearch先根据用户的查询组合返回查询结果到查询页面。用户点击特定的订单,就能根据订单ID去HBase获取订单的全量数据。
2)增量数据迁移的高可用性和一致性:如果是自己编写迁移的代码,那就参考前面冷热分离和查询分离的迁移逻辑;也可以使用开源工具,这个方案在后面数据同步的场景中会单独展开。
3)短时订单量大爆发:分表分库可以解决数据量大的问题,但是如果瞬时流量非常大,数据库撑不住怎么办?这一问题会在后面的缓存和秒杀架构等场景中专门展开。