分库分表不是一道“要不要上”的选择题,而是一道“什么时候上、怎么上”的工程题。过早引入分库分表会让系统架构过度复杂,过晚引入则可能让数据库成为单点瓶颈。理解演进路线和扩容方案,能帮助你在业务增长的不同阶段做出合理决策。
27.4.1 分库分表演进路线:从单机到分布式
绝大多数系统的数据架构都遵循一条相似的演进路径,你不需要在项目初期就把架构设计得极其复杂。
第一阶段:单库单表
业务起步时,数据量和访问量都很少,一个 MySQL 实例完全可以胜任。此时应该把精力放在合理的表设计、索引优化和 SQL 质量上,而不是急着搞分布式架构。单库单表的好处是一切操作都是本地的,事务、JOIN、排序等复杂查询都能高效完成,开发成本极低。
第二阶段:单库多表(垂直拆分)
当业务模块逐渐增多,几张核心表的数据量开始变重,查询和写入压力增大,但单库整体还能承受。此时可以考虑垂直拆分:把一个库里的多张表按业务模块拆到不同的库中,比如用户信息库、商品信息库、订单库各自独立部署。
垂直拆分的好处是拆得干净,不同业务库之间耦合度低,各自可以独立优化和扩容。缺点是跨库 JOIN 不再可能,需要应用层处理数据聚合,或者通过数据冗余来替代关联查询。
第三阶段:读写分离
随着用户量增长,读请求远远超过写请求,主库的查询压力会成为瓶颈。此时在垂直拆分的基础上,进一步引入读写分离:主库只处理写入,一个或多个从库分担读请求。这个阶段一般不涉及分表,还是单库里的一张完整表。
读写分离通过主从复制实现,中间件(如 ProxySQL、ShardingSphere)负责自动路由。这个方案能显著提升读吞吐,且对应用侵入小,是成本最低的扩展方式。
第四阶段:水平拆分(分库分表)
当单表数据量超过千万级别,或者写入 QPS 超过单库承受能力,即使读写分离也无法从根本上解决问题。这时必须进行水平拆分,将同一张表的数据按照某种规则分散到多个库的多张表中。
一张大表被拆成 N 张结构完全相同的小表,每张小表只存放一部分数据。查询时根据分片键(如用户 ID、订单 ID)路由到对应的库和表。水平拆分能够线性扩展存储容量和写入能力,是所有方案中扩展性最强的,也是实现最复杂的。
小结:演进并非线性的,实际项目中垂直拆分、读写分离和水平拆分往往会叠加使用。一个典型的成熟架构可能是:业务按模块垂直分库,每个核心库配置一主多从并启用读写分离,其中部分大表再进行水平分表。
27.4.2 扩容的挑战:数据迁移是真正的难题
水平拆分之后,最头疼的问题不是怎么拆,而是拆完之后怎么扩。当业务继续增长,原本计划的 8 个分片不够用了,需要扩容到 16 个甚至更多,这时就面临一个核心难题:数据需要重新分布,但业务不能停。
假设你原本用 user_id % 8 进行分片,现在要扩容到 user_id % 16,那么几乎所有数据的归属都会改变,需要将很多行从一个分片移动到另一个分片。这个过程如果操作不当,会导致数据不一致、服务中断,甚至数据丢失。
因此,扩容方案的选择直接决定了分库分表方案的成败。以下是几种实践中常见的扩容方式及其优劣。
27.4.3 常见扩容方案
方案一:停机迁移
简单粗暴,停止服务,用脚本将全量数据按新分片规则重新导出导入,验证通过后重新上线。优点是实现简单、无数据不一致风险;缺点是需要停机窗口,且数据量越大停机时间越长。只适用于业务允许离线维护的场景,比如内部管理系统,或者有明确的低峰期。
方案二:成倍扩容法(2^n 倍数扩)
如果分片键取值比较均匀,可以采用“翻倍扩容”的思路来避免全量数据迁移。具体做法是:
- 初始分片数为 2 的幂次,比如 4 个库。
- 当需要扩容时,直接将分片数翻倍为 8 个。但不是把所有库的数据都打散,而是只拆分其中一部分。比如可以将原来的分片 0 拆分为 0 和 4,原来 hash 值模 4 为 0 的数据,现在模 8 为 0 和 4 的数据分别放在两个新库。
- 实际上只需要迁移一半的数据,另一半保持不动。
这种方案减少了迁移量,但分片数受限,不够灵活,且需要自定义路由算法,中间件支持度有限。
方案三:一致性哈希
一致性哈希是分布式系统中经典的扩容友好算法。它的核心思想是将分片节点映射到一个环上,数据根据哈希值顺时针找到最近的节点存储。当新增节点时,只有少部分数据需要迁移到新节点,其余数据保持不动。
不过原生一致性哈希存在热点倾斜问题,所以实践中都会引入虚拟节点:每个物理节点对应多个虚拟节点,均匀分布在哈希环上。这样扩容时数据均衡性更好。
一些中间件如 ShardingSphere 支持自定义分片算法,可以实现一致性哈希分片。但一致性哈希对范围查询的支持较差,如果你的业务中有大量按照分片键范围扫描的需求,需要谨慎评估。
方案四:基于范围的分片天然易于扩容
如果你采用范围分片,比如按时间分片(每年或每月一个库),那么扩容几乎不需要迁移历史数据。新增的时间段直接落在新库上,旧数据不用动。但这种分片方式容易造成写热点集中在最新库上,需要配合其他手段解决。
方案五:基于中间件的平滑扩容(推荐)
现在主流的数据库中间件(如 ShardingSphere、Vitess)已经提供了相对成熟的弹性伸缩能力,可以实现在线扩容,核心流程大致如下:
- 部署新节点:准备好扩容后的新数据库实例。
- 双写双读:修改分片路由配置,将属于待迁移数据的写操作同时发往旧库和新库,读操作仍从旧库读取。
- 历史数据迁移:启动迁移作业,将需要迁移的数据从旧库拷贝到新库,并进行数据校验。
- 切读:迁移和校验完成后,将读流量切换到新库。
- 下线旧库:待稳定运行后,移除双写配置,旧库中已完成迁移的分片可下线回收。
整个过程可以在业务低峰期逐步进行,将一次全量停机的大操作变成多个小步骤的灰度操作,风险可控。以 ShardingSphere 的 Scaling 功能为例,它已经实现了上述流程的自动化,你只需要在管理端配置规则,它就会调度迁移任务并实时校验。
27.4.4 一个实际的扩容操作示例
假设你们当前使用 4 个数据库,每个库 16 张订单表(按 order_id % 16 分表),总共 64 个物理分片。现在要扩容到 8 个库,保持每库 16 张表。
按以下步骤操作(以中间件为例):
- 准备阶段:创建 8 个新的数据库实例,初始化表结构(与旧库完全一致)。
- 变更路由规则:在中间件中将分片规则从 “4 库 × 16 表” 修改为 “8 库 × 16 表”,但此时先不启用新规则,依然使用旧规则进行全部读写。
- 开启迁移:通过中间件的数据迁移功能,将原 4 库中的数据逐步迁移到新 8 库中。迁移期间业务读写不受影响,中间件会自动处理双写和数据回源。
- 数据校验:迁移完成后,对数据进行校验对比,确保新旧库一致。
- 灰度切换:先选择少量流量(比如 5% 的用户)切换到新分片规则,观察监控和业务反馈。
- 全量切换:确认无误后,逐步放量直至所有流量都走新规则。
- 下线旧库:稳定运行一段时间后,清理旧的 4 个数据库实例。
整个过程中业务基本无感知,只是在切换瞬间可能有短暂的响应延迟或等待,但不会有数据丢失。
27.4.5 扩容中的关键注意事项
- 分片键的选择从一开始就决定了扩容的难易。尽量选择分布均匀、无热点的字段,且该字段在查询中高频出现。不要因为某个字段“看起来好扩容”就用它,否则后续查询会很痛苦。
- 不要频繁小幅度扩容。每次扩容都有运维成本和风险,合理的做法是提前规划容量,一次扩容到足够用一段时间,比如够用半年到一年。
- 保障数据一致性。迁移过程中一定要有数据校验和回滚方案,发现不一致能及时修复。迁移脚本要具备幂等性,防止重复迁移导致数据错误。
- 业务兼容性。扩容后某些需要跨全量分片的统计查询(如总额统计)可能变得更慢,要提前评估并设计好汇总表或异步统计方案。
- 监控先行。扩容前后的 QPS、延迟、错误率都要有对比,才能第一时间发现异常。
分库分表的演进是一个持续性工程,没有一劳永逸的方案。理解从单库到分片的完整路径,以及每种扩容方式的成本和风险,能让你在需要扩展时更胸有成竹,而不是被业务逼着临时抓瞎。