第3章 分表分库
第2章讲到,查询分离的方案存在三大不足,其中一个就是:当主数据量越来越大时,写操作会越来越缓慢。这个问题该如何解决呢?可以考虑分表分库。
本章先介绍真实的业务场景,而后依次介绍拆分存储时如何进行技术选型、分表分库的实现思路是什么,以及分表分库存在哪些不足。

3.1 业务场景:亿级订单数据如何实现快速读写
这次项目的对象是电商系统。该系统中大数据量的实体有两个:用户和订单。
| 数据实体 | 数据量 |
|---|---|
| 用户 | 千万级 |
| 订单 | 即将上亿 |
某天,领导召集IT部门人员开会,说:"根据市场推广的趋势,我们的订单很快就会上亿,每天会有 100万 的新订单。不要问我这个数据怎么出来的,总之,领导交代,让IT部门提前做好技术准备,以防到时候系统撑不住"。
做这个规划之前,存储订单的数据库表是一个单库单表。可以预见,在不久的将来数据库的I/O和CPU就可能支撑不住,因为订单系统原来就不是很快。
然后项目组做了简单的功能,插入一些测试数据,订单量到 2000万 的时候,响应时长就不可接受了。
为了使系统能承受这种日百万级新订单的压力,项目组探讨过很多解决方案,最终决定使用分表分库:
- 原来的订单表就是一个 sale 数据库里面的一张 order 表
- 之后创建多个 order 数据库:order1,order2,order3,order4,……
- 每个数据库里面又有多张订单表:t_order_1,t_order_2,t_order_3,……
- 订单子表也是多张:t_order_item_1,t_order_item_2,t_order_item_3,……
订单数据根据一定的规律分布存储在不同 order 库里的不同 order 表中。
3.2 拆分存储的技术选型
拆分存储常用的技术解决方案目前主要分为 4 种:MySQL的分区技术、NoSQL、NewSQL、基于MySQL的分表分库。

3.2.1 MySQL的分区技术
MySQL的分区技术主要体现在文件存储层 File System,它可以将一张表的不同行存放在不同的存储文件中,这对使用者来说比较透明。
在以往的项目中,项目组不使用它的原因主要有 3 点:
- 单实例瓶颈:MySQL的实例只有一个,它仅仅分摊了存储,无法分摊请求负载
- 跨分区性能:用户在实际操作时往往不太注意,如果SQL跨了分区,操作就会严重影响系统性能
- 功能限制:MySQL还有一些其他限制,比如不支持 query cache、位操作表达式等
3.2.2 NoSQL
比较典型的NoSQL数据库就是 MongoDB。MongoDB的分片功能从并发性和数据量这两个角度已经能满足一般大数据量的需求,但是还需要注意以下 3 点:
| 考量维度 | 问题 |
|---|---|
| 约束考量 | MongoDB不是关系型数据库而是文档型数据库,订单数据必须使用强约束的关系型数据库进行存储 |
| 业务功能 | 订单这种跟交易相关的数据必须支持事务和并发控制,而这些并不是MongoDB的强项 |
| 稳定性 | 人们对MySQL的运维已经很熟悉,MongoDB的稳定性无法保证 |
3.2.3 NewSQL
NewSQL技术还比较新,笔者曾经想在一些不重要的数据中使用NewSQL(比如 TiDB),但从稳定性和功能扩展性两方面考量后,最终没有使用,具体原因与MongoDB类似。
3.2.4 基于MySQL的分表分库
分表是将一份大的表数据进行拆分后存放至多个结构一样的拆分表中;分库就是将一个大的数据库拆分成类似于多个结构的小数据库。
项目组选择了基于MySQL的分表分库,其中有一个重要考量:分表分库对于第三方依赖较少,业务逻辑灵活可控,它本身并不需要非常复杂的底层处理,也不需要重新做数据库,只是根据不同逻辑使用不同SQL语句和数据源而已,因此,之后出问题的时候也能够较快地找出根源。
如果使用分表分库,有 3 个通用技术需求需要实现:
- SQL组合:因为关联的表名是动态的,需要根据逻辑组装动态的SQL
- 数据库路由:因为数据库名也是动态的,需要通过不同的逻辑使用不同的数据库
- 执行结果合并:有些需求需要通过多个分库执行后再合并归集起来
Proxy模式与Client模式
目前能解决以上问题的中间件分为两类:

Proxy模式(如 ShardingSphere-Proxy):
- 将SQL组合、数据库路由、执行结果合并等功能全部放在代理服务中
- 优点:对业务代码无侵入,业务只需要关注自身业务逻辑
Client模式(如 Sharding-JDBC):
- 将分表分库相关逻辑放在客户端,客户端的应用引用一个jar
- 优点:高性能,无额外网络跳转,轻量级
| 模式 | 代表中间件 | 优点 | 缺点 |
|---|---|---|---|
| Proxy | ShardingSphere-Proxy, MyCat | 对业务代码无侵入、语言无关 | 额外运维成本、性能有损耗 |
| Client | Sharding-JDBC | 高性能、代码灵活可控 | 需要引入依赖、语言相关 |
因为看重"代码灵活可控"这个优势,项目组最终选择了 Client模式 里的 Sharding-JDBC 来实现分表分库。
3.3 分表分库实现思路
技术选型这一难题解决后,具体如何落实分表分库方案呢?需要考虑 5 个要点:
- 使用什么字段作为分片主键?
- 分片的策略是什么?
- 业务代码如何修改?
- 历史数据如何迁移?
- 未来的扩容方案是什么?
3.3.1 使用什么字段作为分片主键
在选择分片主键之前,首先要了解系统中的一些常见业务需求:
- 用户需要查询所有订单,订单数据中包含不同的 user_ID、order_time
- 后台需要根据城市查询当地的订单
- 后台需要统计每个时间段的订单趋势
根据这些常见业务需求,判断一下优先级,用户操作(也就是第一个需求)必须优先满足。
使用 user_ID 作为订单的分片主键,就能保证每次用户查询数据时,在一个分库的一个分表里即可获取数据。
选择字段作为分片主键时,一般需要考虑 3 个要求:
- 数据尽量均匀分布在不同的表或库
- 跨库查询操作尽可能少
- 所选字段的值不会变(这点尤为重要)
3.3.2 分片的策略是什么
决定使用 user_ID 作为订单分片主键后,就要开始考虑使用何种分片策略。

目前通用的分片策略分为 3 种:
1. 根据范围分片
比如 user_ID 是自增型数字,把 user_ID 按照每100万份分为一个库,每10万份分为一个表的形式进行分片:
| user_ID 范围 | 数据库 | 表 |
|---|---|---|
| 1-100万 | order1 | t_order_1 ~ t_order_10 |
| 100万-200万 | order2 | t_order_1 ~ t_order_10 |
2. 根据Hash值分片
根据 user_ID 的 Hash 值 mod(取模)一个特定的数进行分片(为了方便后续扩展,一般是 2^n)。
3. 根据Hash值及范围混合分片
先按照范围分片,再根据Hash值取模分片。比如:
表名 = order_#{user_ID % 10}_#{hash(user_ID) % 8}
即分成了 10 × 8 = 80 个表。
选择分片策略时,只需要考虑一点:假设之后数据量变大了,需要把表分得更细,此时保证迁移的数据尽量少即可。
因此,根据Hash值分片时,一般建议拆分成 2^n 个表。比如分成8张表,数据迁移时把原来的每张表拆一半出来组成新表,这样数据迁移量就小了。
当初的方案中,就是根据 user_ID 的 Hash 值按 32 取模,把数据分到 32 个数据库中,每个数据库再分成 16 张表。
简单计算一下:
- 假设每天订单量为 1000万
- 每个库日增:1000万 / 32 ≈ 31.25万
- 每个表日增:1000万 / 32 / 16 ≈ 1.95万
- 3年后每个表的数据量:约 2000万左右,仍在可控范围内
3.3.3 业务代码如何修改
分片策略确定后,需要注意以下几个要点:
-
微服务架构优势:如果使用微服务,对于特定表的分表分库,其影响面只为该表所在的服务。而如果是一个单体架构的应用做分表分库,那会很麻烦,因为单体架构里面会有很多的跨表关联查询
-
避免外键约束:在互联网架构中,基本不使用外键约束
-
避免跨库跨表查询:分库分表以后,与订单有关的一些读操作都要考虑对应的数据是在哪个库哪个表,尽量避免跨库或跨表查询
3.3.4 历史数据如何迁移
历史数据的迁移非常耗时,迁移几天几夜都很正常。而在互联网行业中,别说几天几夜,就算停机几分钟,业务都可能无法接受,这就要求给出一个无缝迁移的解决方案。

数据迁移方案的基本思路:旧架构继续运行,存量数据直接迁移,增量数据监听binlog,然后通过 Canal 通知迁移程序迁移数据,等到新的数据库拥有全量数据且校验通过后再逐步切换流量到新架构。
数据迁移解决方案的详细步骤如下:
- 上线 Canal:通过 Canal 触发增量数据的迁移
- 迁移存量数据:迁移数据脚本测试通过后,将老数据迁移到新的分表分库中
- 确保数据完整:注意迁移增量数据与迁移老数据的时间差,确保全部数据都被迁移过去,无任何遗漏
- 数据验证:此时新的分表分库中已经拥有全量数据了,运行数据验证程序,确保所有数据都存放在新数据库中
- 代码上线:之后就是新版本代码上线,至于是灰度上线还是直接上线,需要根据实际情况决定
3.3.5 未来的扩容方案是什么
随着业务的发展,如果原来的分片设计已经无法满足日益增长的数据量的需求,就需要考虑扩容了。
扩容方案主要依赖以下两点:
-
分片策略设计:分片策略是否可以让新表数据的迁移源只有一个旧表,而不是多个旧表?这就是前面建议使用 2^n 分表的原因——以后每次扩容都能扩为2倍,都是把原来一张表的数据拆分到两张表中
-
数据迁移:需要把旧分片的数据迁移到新的分片上,方案与历史数据迁移一样
3.4 小结

分表分库的解决方案就讲完了,这也是业界常用的做法。这个方案实现以后,项目组对它做了一些压力测试,1亿订单量的情况下,基本上也能做到20毫秒之内响应。
后来,随着业务的发展,在分表分库系统上线的11个月后,日订单量达到了100万。事实证明,在大数据时代,提前考虑大数据量的到来是必要的。
方案的不足
不过该方案还有一些不足之处:
-
复杂查询慢:很多查询需要跨订单数据库进行,然后再组合结果集,这样的查询比较慢。业界的普遍做法是结合查询分离,将查询数据存到 Elasticsearch 和 HBase 中
-
增量数据迁移的高可用性和一致性:如果是自己编写迁移的代码,那就参考前面冷热分离和查询分离的迁移逻辑;也可以使用开源工具
-
短时订单量大爆发:分表分库可以解决数据量大的问题,但是如果瞬时流量非常大,数据库撑不住怎么办?这一问题会在后面的缓存和秒杀架构等场景中专门展开
不能期望一个解决方案既能覆盖所有的问题,还能实现最小的成本损耗。如果碰到一个场景不能接受上面某个或某些不足时,该怎么解决?请看下一章:读缓存。