前言
我们在上一章讲到,查询分离的方案存在三大不足,其中一个就是:当主数据量越来越大时,写操作会越来越缓慢。这个问题该如何解决呢?可以考虑分表分库。
今天我们会先介绍一下真实的业务场景,而后依次介绍拆分存储时如何进行技术选型、分表分库的实现思路是什么,以及分表分库存在哪些不足。
1.业务场景:亿级订单数据快速读写
这次项目是电商项目,核心领域表是用户表喝订单表。业务部门交代 :”订单表很快就会上亿、日均订单量将达到百万级“。
再此背景下经过测试,向订单量插入数据,到2000万的时候,响应时长就不可接受了。为了让系统支撑达到百万级别的写入压力,项目组深入探讨之后决定进行分库分表 (先将订单表拆分,再进行分布存储)。
原本订单表是sale库下的order表,分库分表后变为 多个order库的下的多个order表. 当然 订单子表也是多张。 sale.order -> order. [ t_order_1 , t_order_2 ... ]
2.拆分存储的技术选型
拆分存储常用的技术解决方案目前主要分为4种:MySQL的分区技术、NoSQL、NewSQL、基于MySQL的分库分表。
2.1 MySQL分区
下图为MySQL官方架构图,MySQL的分区技术主要体现在图中的文件存储层File System,它可以将一张表的不同行存放在不同的存储文件中。

此次项目背景不适用MySQL分区的原因:
- 仅仅分摊了存储,无法分摊请求负载 。分区只解决了数据存储的位置(分散到不同硬盘文件),但没有增加处理数据的能力。 这就像把仓库的货物从一个大房间分到了几个小房间,但搬运工还是原来那一个人。当很多人来提货时,这个唯一的搬运工依然会忙不过来,瓶颈并没有解决。
- 于透明性导致性能问题。系统自动在多个分区中搜索,一个坏的查询会从搜索一次变成搜索多次,效率急剧下降。
- **还有一些其他限制。 **比如不支持query cache、位操作表达式等。
2.2 NoSQL
典型的NoSQL数据库就是MongoDB。MongoDB的分片功能从并发性和数据量这两个角度已经能满足一般大数据量的需求,但是还需要注意下面3点。
- 约束考量(规矩要严,不能太“灵活”):订单这类核心数据,格式必须严格固定(比如金额、状态等字段一个都不能少),就像一份具有法律效力的合同。MySQL等关系型数据库像一份印刷好的标准合同,格式固定,能自动检查错误,防止填错或漏填。而MongoDB默认像一叠白纸,虽然想写什么就写什么很灵活,但也容易写漏或写错关键信息,不适合记录像订单这样严肃重要的数据。
- 业务功能考量(关键操作要“万无一失”):订单这种跟交易相关的数据肯定要支持事务和并发控制,而这些并不是MongoDB的强项。
- 稳定性考量(系统要稳,经验很重要):MySQL就像一辆被无数司机开过的经典车型,它的性能和毛病大家都一清二楚,维修保养经验丰富。而MongoDB则像一辆功能新潮但路上还不多见的新概念车,虽然可能很棒,但稳定性如何、出了问题怎么修,知道的人还不多,因此不敢轻易用它来执行“关键任务”
针对本次业务场景,基于以上的原因,当时项目组排除了MongoDB。
3.3 NewSQL
时间来到2025年,NewSQL生态已更为成熟,出现了多个具有代表性的产品,它们在不同场景下验证了自身的价值。例如:
| 数据库 | 核心特点与场景 |
|---|---|
| 阿里系 OceanBase | 蚂蚁集团自研的分布式数据库,以金融级高可用和稳定性著称,历经“双十一”超高并发场景的持续验证。其发布的一体化架构甚至开创性地实现了TP(事务处理)与AP(分析处理)能力的统一。 |
| PingCAP 的 TiDB | 开源的分布式数据库,兼容MySQL生态,支持强一致性事务与水平扩展。其新版特性强化了AI向量搜索能力,致力于成为AI就绪的数据平台。 |
| Google Cloud Spanner | 全球分布的分布式数据库,其核心技术TrueTime API解决了跨数据中心的数据一致性问题,适合业务全球化的企业。 |
| CockroachDB | 架构灵感源于Google Spanner的开源实现,以高度的容错性和灵活的地理分布部署能力为核心特点。 |
尽管上述NewSQL数据库在可扩展性、SQL兼容性和强一致性事务方面表现出色,但对于许多传统企业或核心业务系统而言,技术选型的决策天平依然会倾向于稳定压倒一切的一方。这与之前放弃MongoDB的逻辑一脉相承。
3.4 基于MySQL的分表分库
基于MySQL分表分库对于第三方依赖较少,业务逻辑灵活可控,它本身并不需要非常复杂的底层处理,也不需要重新做数据库,只是根据不同逻辑使用不同SQL语句和数据源而已,因此,之后出问题的时候也能够较快地找出根源。
3.4.1 技术问题
如果使用分表分库,有3个通用技术需求需要实现:
1. SQL组合:(“生成快递地址”):数据(比如订单)被分散存放在很多张名字类似的表里,比如 <font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">t_order_1</font>, <font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">t_order_2</font> ... <font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">t_order_100</font>。程序要查询一个订单时,它无法直接知道这个订单具体在哪个“房间”(表)里。
2. 数据库路由:(“选择去哪栋楼”): 数据库也不止一个,可能分了多个库,比如 <font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">order_db_1</font>, <font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">order_db_2</font>。拼好了SQL,知道了去 <font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">t_order_5</font> 这个房间找,但程序该连接 <font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">order_db_1</font> 还是 <font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">order_db_2</font> 呢?也就是需要先算后连,找到正确的“楼栋”(数据库)并建立连接。
3. 执行结果合并:(“跨店调货汇总”): 有些查询条件很宽泛(比如“查询所有待付款订单”),它无法限定在某一张表或某一个库内。需要分头查找,再把零散的结果汇总成一份完整的答案。
3.4.2 两种中间件模式
目前能解决以上问题的中间件分为两类:Proxy模式、Client模式。
1.Proxy模式:
Proxy模式中,应用服务像连接普通MySQL一样连接Proxy,所有分片逻辑都在Proxy内部处理,对应用完全透明。
这种设计模式将SQL组合、数据库路由、执行结果合并等功能全部放在了一个代理服务中,而与分表分库相关的处理逻辑全部放在了其他服务中,其优点是对业务代码无侵入,业务只需要关注自身业务逻辑即可。
2.Client模式
Client模式中,分片逻辑以SDK形式嵌入应用服务,应用直接连接各个分片数据库,性能更高但需要代码改造。
这种设计模式将分表分库相关逻辑放在客户端,一般客户端的应用会引用一个jar,然后在jar中处理SQL组合、数据库路由、执行结果合并等相关功能。
2025年分库分表中间件概览:
| 中间件 | 模式 | 厂家 | 语言 |
|---|---|---|---|
| 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 |
| Vitess | Proxy | PlanetScale | Go |
| ProxySQL | Proxy | 开源项目 | C++ |
| Sharding-Proxy | Proxy | Apache ShardingSphere | Java |
Proxy vs. Client 模式优缺点对比
| 特性 | Proxy (代理) 模式 | Client (客户端) 模式 |
|---|---|---|
| 架构位置 | 独立部署的进程,介于应用与数据库之间。 | 嵌入到客户端应用程序中。 |
| 优点 | 1. 对应用透明:应用像连接单一数据库,无需感知分库分表逻辑。 2. 支持多语言 3. 升级方便:中间件独立升级,不影响业务应用。 | 1. 性能更高:无网络中转,直连数据库,链路更短,延迟更低。 2. 运维简单:无需部署和维护独立的代理服务。 3. 代码灵活可控:分片逻辑在应用内,更易于定制和精细控制。 |
| 缺点 | 1. 性能有损耗:多一次网络转发,存在带宽瓶颈风险。 2. 运维复杂:需独立部署、监控并保证其高可用 3. 线上调试难:问题可能出现在应用、Proxy或数据库,排查链路更长。 | 1. 侵入性强:分片逻辑与业务代码耦合,需要引入特定SDK。 2. 仅支持单语言 3. 升级不便:中间件升级需要随应用一起发布、重启。 |
因为看重“代码灵活可控”这个优势,项目组最终选择了Client模式里的Sharding-JDBC来实现分表分库。
3.分表分库实现思路
技术选型确定后,具体如何落实分表分库方案呢?需要考虑5个要点。
1)使用什么字段作为分片主键?
2)分片的策略是什么?
3)业务代码如何修改?
4)历史数据如何迁移?
5)未来的扩容方案是什么?
3.1 使用什么字段作为分片主键
订单表核心数据结构包括
- user_id 用户id
- order_id 订单id
- user_city_id 用户所在城市id
- order_time 下单时间
最终使用user_ID作为分片主键。
思路如下。在选择分片主键之前,首先要了解系统中的一些常见业务需求。
1)用户需要查询所有订单。
2)后台需要根据城市查询当地的订单。
3)后台需要统计每个时间段的订单趋势。
根据需求重要性优先级,用户操作需求(第一个需求)最重要,必须满足,此时如果使用user_ID作为订单的分片主键,就能保证每次用户查询数据时,在一个分库的一个分表里即可获取数据。
TIPS: 选择字段作为分片主键时,一般需要考虑3个要求:数据尽量均匀分布在不同的表或库、跨库查询操作尽可能少、所选字段的值不会变(这点尤为重要)。
3.2 分片的策略是什么
目前通用的分片策略分为根据范围分片、根据Hash值分片、根据Hash值及范围混合分片这3种。
1)根据范围分片:比如user_ID是自增型数字,把user_ID按照每100万份分为一个库,每10万份分为一个表的形式进行分片

2)根据Hash值分片 : 指的是根据user_ID的Hash值mod(取模)一个特定的数进行分片(_为了方便后续扩展,一般是2的n次幂_)。
**3)根据Hash值及范围混合分片 : **先按照范围分片,再根据Hash值取模分片
策略选择的原则就是将来扩容需要更多分表的时候尽可能的迁移更少的数据。当初的方案中,就是根据user_ID的Hash值按32取模,把数据分到32个数据库中,每个数据库再分成16张表。简单计算一下,假设每天订单量为1000万,则每个库日增1000万/16=31.25万,每个表日增1000万/32/16=1.95万,3年后每个表的数据量就是2000万左右,仍在可控范围内。
如果业务增长特别快,且运维还能承受,为避免以后出现扩容问题,建议库分得越多越好。
3.3 业务代码如何修改
业务代码修改与业务强关联,该项目采用的方案不具备通用性,近年来分表分库操作更加容易不过可以在这里分享几个注意点:
1.微服务下拆分更简单 : 用微服务做分库分表,订单表只影响订单服务这一个应用。如果是大单体架构,到处都有跨表联查(Join),订单表一拆,到处都要改代码,非常麻烦。‘
2.尽量不用外健约束
3.避免跨库跨表查询 :分库分表后,所有订单的查询操作,必须先定位到具体的库和表。设计上要尽量避免跨库、跨表查询,否则会严重拖慢性能。
3.4 历史数据如何迁移
历史数据的迁移非常耗时,迁移几天几夜都很正常。下面给出一个无缝迁移的方案。
讲解查询分离时提过一个方案,就是监控数据库变更日志,将数据库变更的事件变成消息,存到消息系统,然后有个消费者订阅消息,再将变动的数据同步到查询数据库。

历史数据迁移就可以采用类似的方案,只不过将更新查询库变为迁移到新增的分库。

- 基本思路:旧架构继续运行,存量数据直接迁移,增量数据监听binlog,然后通过canal通知迁移程序迁移数据,等到新的数据库拥有全量数据且校验通过后再逐步切换流量到新架构。
- 详细步骤:
- 上线canal,通过canal触发增量数据的迁移。
- 迁移数据脚本测试通过后,将老数据迁移到新的分表分库中。
- 注意迁移增量数据与迁移老数据的时间差,确保全部数据都被迁移过去,无任何遗漏。
- 此时新的分表分库中已经拥有全量数据了,可以运行数据验证程序,确保所有数据都存放在新数据库中。
- 迁移完成,新版本代码上线。
3.5 未来的扩容方案是什么
随着业务的发展,如果原来的分片设计已经无法满足日益增长的数据量的需求,就需要考虑扩容了。扩容方案主要依赖以下两点。
1)分片策略是否可以让新表数据的迁移源只有一个旧表,而不是多个旧表?这就是前面建议使用2n次幂分表的原因——以后每次扩容都能扩为2倍,都是把原来一张表的数据拆分到两张表中。原来一张表的数据,下次扩容时只需平分成两份,各自迁入两个新表。数据来源单一,迁移路径明确,不会出现“父子关系混乱”(从多个旧表迁入一个新表)。
2)数据迁移。需要把旧分片的数据迁移到新的分片上,这个方案与上面提及的历史数据迁移大体一样,值得注意的是首次分片是将历史数据全量迁移到新分片中,优势就是方便回滚,后期扩容迁移也可以不迁移全量,可以根据扩容的数量,按需迁移要迁移的数据,这里不再赘述。
4.小结
分表分库是业界常用的做法。当前方案在1亿订单量的情况下,基本上也能做到20毫秒之内响应。这个方案在订单读写层面基本是足够的,至少保证了数据库不会宕机,不会因为订单量大系统就撑不住。
不过该方案还有一些不足之处。
1)复杂查询慢(需要跨库查询):很多查询需要跨订单数据库进行,然后再组合结果集,这样的查询比较慢。业界的普遍做法是前面提到的查询分离。第2章讲了单独使用Elasticsearch做查询分离的方案,这里分表分库的二期项目也进行了查询分离,只是查询数据存到了Elasticsearch和HBase中。Elasticsearch存放订单ID、用来查询关键字的字段以及查询页面列表里用到的字段,HBase存放订单的全量数据。Elasticsearch先根据用户的查询组合返回查询结果到查询页面。用户点击特定的订单,就能根据订单ID去HBase获取订单的全量数据。
2)增量数据迁移的高可用性和一致性(迁移可靠性):可以直接套用前面“冷热/查询分离”的迁移逻辑,或者采用后续介绍的开源工具,保证过程稳定、数据一致。这个方案在后面数据同步的场景中会单独展开。
3)瞬时流量冲击(短时订单量大爆发):分表分库可以解决数据量大的问题,但是如果瞬时流量非常大,数据库撑不住怎么办?这是数据库层无法独自解决的,需要依靠后续介绍的缓存和秒杀架构来扛住瞬时流量。
至此,数据持久化层的所有场景就介绍完了。之后将进入缓存层场景实战。
我们在上一章讲到,查询分离的方案存在三大不足,其中一个就是:当主数据量越来越大时,写操作会越来越缓慢。这个问题该如何解决呢?可以考虑分表分库。今天我们会先介绍一下真实的业务场景,而后依次介绍拆分存储时如何进行技术选型、分表分库的实现思路是什么,以及分表分库存在哪些不足。