软件架构场景之—— 分表分库:单表数据量大读写缓慢如何解决?

业务背景

一个电商系统的架构优化,该系统中包含用户和订单 2 个主要实体,每个实体涵盖数据量如下表所示

实体 数据量 增长趋势
用户 上千万 每日十万
订单 上亿 每日百万级速度增长,之后可能是千万级

从上表中我们发现,目前订单数据量已达上亿,且每日以百万级的速度增长,之后还可能是千万级。面对如此庞大的数据量,此时存储订单的数据库表竟然还是一个单库单表。对于单库单表而言,一旦数据量实现疯狂增长,无论是 IO 还是 CPU 都会扛不住

为了使系统抗住千万级数据量的压力,尝试过很多解决方案,最终我们想到了先将订单表拆分,再进行分布存储,说到分表分库解决方案,首先需要做的就是搞定拆分存储的技术选型问题

拆分存储的技术选型

关于拆分存储常用的技术解决方案,市面上目前主要分为 4 种:MySQL 的分区技术、NoSQL、NewSQL、基于 MySQL 的分表分库

1.MySQL 的分区技术

从上面的 MySQL 架构图中,不难发现 MySQL 的分区主要在文件存储层做文章,它可以将一张表的不同行存放在不同存储文件中,这对使用者来说比较透明

在以往的实战项目中,我们不使用它的原因主要有三点

  1. MySQL 的实例只有一个,它仅仅分摊了存储,无法分摊请求负载

  2. 正是因为 MySQL 的分区对用户透明,所以用户在实际操作时往往不太注意,使得跨分区操作严重影响系统性能

  3. 当然,MySQL 还有一些其他限制,比如不支持 query cache、位操作表达式等。参考文章:https://dev.mysql.com/doc/refman/5.7/en/partitioning-limitations.html

2.NoSQL(如 MongoDB)

比较典型的 NoSQL 数据库就是 MongoDB 啦。MongoDB 的分片功能从并发性和数据量这两个角度已经能满足一般大数据量的需求,但是还需要注意这三大要点

  • 约束考量: MongoDB 不是关系型数据库而是文档型数据库,它的每一行记录都是一个结构灵活可变的 JSON,比如存储非常重要的订单数据时,我们就不能使用 MongoDB,因为订单数据必须使用强约束的关系型数据库进行存储

  • 业务功能考量: 多年来,事务、锁、SQL、表达式等千奇百怪的操作都在 MySQL 身上一一验证过, MySQL 可以说是久经考验,因此在功能上 MySQL 能满足我们所有的业务需求,MongoDB 却不能,且大部分的 NoSQL 也存在类似问题

  • 稳定性考量: 对 MySQL 的运维已经很熟悉了,它的稳定性没有问题,然而 MongoDB 的稳定性我们没法保证,毕竟不熟悉,因此在之前的拆分存储技术选型过程中,没使用过 NoSQL

3.NewSQL(如 TiDB)

NewSQL 技术还比较新,曾经想在一些不重要的数据中使用 NewSQL(比如 TiDB),但从稳定性和功能扩展性两方面考量后,最终没有使用,具体原因与 MongoDB 类似

4.基于 MySQL 的分表分库

什么是分表分库?

分表是将一份大的表数据拆分存放至多个结构一样的拆分表;分库就是将一个大的数据库拆分成多个结构一样的小库

选择了基于 MySQL 的分表分库,主要是有一个重要考量:分表分库对于第三方依赖较少,业务逻辑灵活可控,它本身并不需要非常复杂的底层处理,也不需要重新做数据库,只是根据不同逻辑使用不同 SQL 语句和数据源而已

如果使用分表分库,存在 3 个技术通用需求需要实现

  • SQL 组合: 因为我们关联的表名是动态的,所以我们需要根据逻辑组装动态的 SQL

  • 数据库路由: 因为数据库名也是动态的,所以我们需要通过不同的逻辑使用不同的数据库

  • 执行结果合并: 有些需求需要通过多个分库执行,再合并归集起来

市面上能解决以上问题的中间件分为 2 类:Proxy 模式、Client 模式

  • (1)Proxy 模式: 直接拿 ShardingSphere 官方文档里的图进行说明,重点看看中间 Sharding-Proxy 层,如下图所示

以上这种设计模式,把 SQL 组合、数据库路由、执行结果合并等功能全部存放在一个代理服务中,而与分表分库相关的处理逻辑全部存放在另外的服务中,这种设计模式的优点是对业务代码无侵入,业务只需要关注自身业务逻辑即可

  • (2)Client 模式: 还是继续借用 ShardingSphere 官方文档里的图来说明,如下图所示

以上这种设计模式,把分表分库相关逻辑存放在客户端,一般客户端的应用会引用一个 jar,然后在 jar 中处理 SQL 组合、数据库路由、执行结果合并等相关功能

关于这两种模式的中间件有如下选择

中间件技术 模式 厂家 语言
MyCat Proxy   Java
KingShard Proxy   Go
Atlas Proxy 360 C
zebra Client 美团 Java
cobar Proxy 阿里 Java
Sharding-JDBC Client Apache ShardingSphere Java
TSharding Client 蘑菇街 Java

简单对比下这 2 个模式的优缺点

模式 优点 缺点
Proxy

1、多语言

2、资源消耗解耦,不需要消耗客户端的资源

3、升级方便

1、多一层服务调用,debug线上问题调查难一些

2、多一层运维成本

Client

1、少一层服务调用,代码灵活可控

2、较少运维成本

1、单语言

2、升级不方便

看重代码灵活可控这个优势,所以最终选择了 Client 模式里的 Sharding-JDBC 来实现分表分库,如下图所示

关于拆分存储选择哪种技术,在实际工作中我们需要根据各自的实际情况来定

分表分库实现思路

技术选型这一大难题解决后,具体如何落地分表分库解决方案成了我们亟待解决的问题

1. 使用什么字段作为分片键?

业务场景中举例的数据库

实体 数据量 增长趋势
用户 上千万 每日十万
订单 上亿 每日百万级增长

我们把上表中的数据拆分成了一个订单表,表中主要数据结构如下

表明 字段 备注
t_order user_id 客户 id
  order_id 订单 id
  user_city_id 用户所在城市 id ,这是一个冗余字段
  order_time 订单时间
  .... ....

从上面表中可知,最终是使用 user_id 作为分片主键,在选择分片字段之前,首先了解了下目前存在的一些常见业务需求

  • 用户需要查询所有订单,订单数据中肯定包含不同的 merchant_id、order_time;

  • 后台需要根据城市查询当地的订单;

  • 后台需要统计每个时间段的订单趋势

根据这些常见业务需求,我们判断了下优先级,用户操作也就是第一个需求必须优先满足。此时,如果我们使用 user_id 作为订单的分片字段,就能保证每次用户查询数据时(第 1 个需求),在一个分库的一个分表里即可获取数据。因此,在我们的方案里,最终还是使用 user_id 作为分片主键,这样在分表分库查询时,首先会把 user_id 作为参数传过来

这里需要特殊说明下,选择字段作为分片键时,我们一般需要考虑三点要求:数据尽量均匀分布在不同表或库、跨库查询操作尽可能少、这个字段的值不会变(这点尤为重要)

2. 分片的策略是什么?

决定使用 user_id 作为订单分片字段后,就要开始考虑分片的策略问题了。目前,市面上通用的分片策略分为:根据范围分片、根据 hash 值分片、根据 hash 值及范围混合分片这三种

  • 根据范围分片

比如用户 id 是自增型数字,我们把用户 id 按照每 100 万份分为一个库,每 10 万份分为一个表的形式进行分片,如下表所示

用户 id 范围 数据库名 表名
0-99999 order_0 t_order_00
100000-199999 order_0 t_order_01
200000-299999 order_0 t_order_02
.... order_0 .....
900000-999999 order_0 t_order_09
1000000-1099999 order_1 t_order_10

特殊说明:这里只讲分表,至于分库把分表分组存放在一个库即可

  • 根据 hash 值分片

指的是根据用户 id 的 hash 值 mod 一个特定的数进行分片(为了方便后续扩展,一般是 2 的几次方)

  • 根据 hash 值及范围混合分片

先按照范围分片,再根据 hash 值取模分片。比如:表名=order_#user_id%10#_#hash(user_id)%8,即被分成了 10*8=80 个表。为了方便你理解,我们画个图说明下,如下图所示

以上三大分片策略我们到底应该选择哪个?

我们只需要考虑一点:假设之后数据量变大了,需要我们把表分得更细,此时保证迁移的数据尽量少即可,因此,根据 hash 值分片时我们一般建议拆分成 2 的 N 次方表。比如分成 8 张表,数据迁移时把原来的每张表拆一半出来组成新表,这样数据迁移量就小了

3. 业务代码如何修改?

分片策略定完后,就要考虑业务代码如何修改了。因修改业务代码部分与业务强关联,近年来,分表分库操作愈发容易,不过需要注意几个要点

  • 我们已经习惯微服务了,对于特定表的分表分库,其影响面只在该表所在的服务中,如果是一个单体架构的应用做分表分库,那真是伤脑筋

  • 在互联网架构中,我们基本不使用外键约束

  • 随着查询分离的流行,后台系统中有很多操作需要跨库查询,导致系统性能非常差,这时分表分库一般会结合查询分离一起操作:先将所有数据在 ES 索引一份,再使用 ES 在后台直接查询数据。如果订单详情数据量很大,还有一个常见做法,即先在 ES 中存储索引字段(作为查询条件的字段),再将详情数据存放在 HBase 中

一般来说,业务代码的修改不会很复杂,最麻烦的是历史数据的迁移

4. 历史数据迁移?

历史数据的迁移非常耗时,有时迁移几天几夜都很正常。而在互联网行业中,别说几天几夜了,就连停机几分钟业务都无法接受,这就要求给出一个无缝迁移的解决方案

查询分离时的方案,如下图所示

历史数据迁移时,采用类似的方案进行历史数据迁移,如下图所示

此数据迁移方案的基本思路:全量数据直接迁移,增量数据监听 binlog,然后通过 canal 通知迁移程序搬运数据,新的数据库拥有全量数据,且校验通过后逐步切换流量

数据迁移解决方案详细的步骤如下

  • 上线 canal,通过 canal 触发增量数据的迁移;

  • 迁移数据脚本测试通过后,将老数据迁移到新的分表分库中;

  • 注意迁移增量数据与迁移老数据的时间差,确保全部数据都被迁移过去,无遗漏;

  • 第二步、第三步都运行完后,新的分表分库中已经拥有全量数据了,这时我们可以运行数据验证的程序,确保所有数据都存放在新数据库中;

  • 到这步数据迁移就算完成了,之后就是新版本代码上线了,至于是灰度上还是直接上,需要根据实际情况决定,回滚方案也是一样

5. 未来的扩容方案是什么?

随着业务的发展,如果原来的分片设计已经无法满足日益增长的数据需求,就需要考虑扩容了,扩容方案主要依赖以下两点

  • 分片策略是否可以让新表数据的迁移源只是 1 个旧表,而不是多个旧表,这就是前面我们建议使用 2 的 N 次方分表的原因;

  • 数据迁移:我们需要把旧分片的数据迁移到新的分片上,这个方案与上面提及的历史数据迁移一样

 

分表分库的不足

  • ES+Hbase 做数据查询分离的方案

  • 增量数据迁移: 如何保证数据的一致性及高可用性

  • 短时订单量大爆发

猜你喜欢

转载自blog.csdn.net/vincent_wen0766/article/details/112367423
今日推荐