基础概述
约 6760 字大约 23 分钟
2025-09-13
单机数据的存储能力和连接数都是有限的,它自身就很容易为成为系统的瓶颈。当单表数据量在百万以里时,还可以通过从库、优化索引提升性能。
分库分表的必要性
容量
对于以MySQL举例子,在绝大多数的情况下,B+树的层数最多为三层。所以我们来粗略估算下三层B+树大概可以记录多少条数据(假设每条数据的大小为1Kb):
- 对于叶子节点而言,它的数据页大小为16KB,每条数据1KB的时候,每个叶子节点可以存储16条数据;
- 对应非叶子节点,它不存储数据,它存储的主键索引以及指向叶子节点的指针,一个指针的大小为6字节,假设主键索引为BigInt(8字节),那么一个键值对就是14KB。所以在非叶子节点中可以存储:$16 \times 1024 \div 14 \approx 1170 $个键值对。(一个非叶子节点可以管理1170个叶子节点)
因为B+树有三层,所以根节点管理了1170个叶子节点,然后1170个二层非叶子节点每个都管理了1170个叶子节点,每个叶子节点都有16条数据,因此总的数据数量就是:1170×1170×16=21902400条数据,因此对于一个三层的B+树最多可以容纳的数据大概在2000万左右,
以上的估算是跟主键索引的类型、单行数据的大小有关系的,通常来讲,在设计表的时候要充分考虑这些因素的,且尽量保持单表数据在2千万以内
随着系统的运行,类似的订单表、账单表等历史记录表,对于某宝这种大型的系统而言,一天的订单数据量就可能在100万级别,所以2000万的数据是极其容易满足的,因此仅仅靠着单表的存储肯定是很难满足的。
仅管在很多的时候我们可以通过其他的方式来减少业务历史表的数据:
- 数据分区:按照时间、地域等规则将表划分为多个分区,分散存储压力(如按月分区);
- 数据归档:将历史数据迁移至归档或冷存储(如ElasticSearch),减少主表的数据量;
但是仍然需要通过分库分表的方式来尽可能减少单表数据量的大小。
性能
一旦数据库朝着千万级别的趋势增长时,再怎么优化数据库,很多的操作性能仍然下降严重。为了减少数据库的负担,提升数据库的响应速度,缩短查询的时间,这时候就需要进行分库分表。
我们来看一个例子,就以订单表来举例子,这个订单表的建表语句为:
CREATE TABLE `orders` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '订单ID',
`order_no` varchar(32) NOT NULL COMMENT '订单编号',
`user_id` bigint NOT NULL COMMENT '用户ID',
`amount` decimal(10,2) NOT NULL COMMENT '订单金额',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '订单状态',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_no` (`order_no`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='订单表';当我们不断的向这个表中插入语句,然后执行下面的SQL语句的时候:
select * from orders where order_no > 'ORD3584009' order by create_time desc limit 10;执行上的SQL语句,它的耗时情况如下所示:
| 数据量 | 耗时 |
|---|---|
| 100万 | < 0.001s |
| 500万 | 1.737s |
| 700万 | 2.340s |
| 900万 | 3.085s |
| 1700万 | 5.096s |
根据测试的结果,很容易看到,在数量超过200万的时候,我们通过索引来查询数据的时候也仍然是非常慢的。尤其是数量大、并发高的情况下,我们是没办法在单表的情况下来保证查询的性能的。
连接数
在MySQL中一个数据库支持的最大连接数可以配置的,我们可以下面的SQL来查询当前数据库配置的最大连接数。
show variables like 'max_connections';仅管在现在的技术发展中,针对连接数的问题,我们可以通过“池化”的思想来解决问题,例如Duriad数据源以及SpringBoot中默认的连接池HikariCP,都是通过池化的技术来减少与数据库创建连接的数量,增加连接的复用性。
但是随着系统体量的不断发展,一个大型的分布式系统通常会聚集大量的服务实例,每个服务实例都需要连接这个数据库,此时就是造成数据库的连接数不够用的情况。因此,使用单库是很难满足大型分布式系统的连接需求的。
什么时候需要分库分表
在了解到分库分表的需求后,紧接着的问题就是:我们需要什么时候对数据进行分库分表?
在阿里巴巴的开发手册中有条建议:单表行数超过200万或者单表数据容量超过2G,才推荐进行分库分表。也就是说,我们需要对我们的表中的数据进行一个初步的估算。例如,我希望这个系统能够稳定的运行5年以上的时候,我们判断这张表的每天增加的数据是多少,根据这种估算得到他在5年后的数据量是多大,然后再决定是否需要分库分表。
像上面的订单表,这种每天增加很多的我们需要对它进行分库分表。但是,还有一些数据表,例如商品分类表,类似这种表通常不会超过1万,我们就是没有必要对其进行分库分表的。
虽然有些时候我们可以选择其他的数据存储服务,例如MongoDB这种高性能的非关系型数据库,但是现在大部分的互联网系统的核心数据几乎都是存储在关系型数据库中(例如MySQL或者Oracle等),因为它的比起非关系型数据库而言,有着无法比拟的可靠性和稳定性,产品成熟生态系统完善,还有核心事务的功能特性,也是其他的存储工具不具备的,而点赞、评论这些非核心数据还是可以考虑MongoDB的。
如何进入分库分表
分库分表的核心就是将数据相对均匀分配在不同的库、表中,以及分片后对数据的快速定位与检索结果的整合。
垂直拆分
垂直分库
垂直分库是数据库架构设计中的一种策略,指的是按照业务功能或数据表的字段特性,将原本存储在同一个数据库中的不同表,拆分到不同的数据库实例中。简单来说,就是按照“列”或者“业务模块”把一个数据库拆分成多个数据库。
垂直分库很大程度上取决于业务的划分,但有时候业务间的划分并不是那么清晰,比如:电商中订单数据的拆分,其它很多的业务都依赖于订单数据,有时候界限不是很好划分。
垂直分库的优点:
- 业务解耦:各业务模块数据独立存储,耦合度低,便于团队分工开发和维护;
- 提升性能:减少单库的负载,不同业务访问不同的库,减少资源争抢;
- 提高可扩展性:可以针对不同业务的模块的数据库做独立的优化和扩展;
- 技术异构:不同的库可以使用不同的数据库技术;
垂直分库的缺点:
- 跨库查询复杂:如果业务需要同时查询多个数据库,需要额外开发跨库逻辑,比如通过分布式事务、数据同步、API聚合等当时解决;
- 事务管理难度增加:跨库事务难以保证一致性,需要引入分布式事务解决方案(如TCC、Saga、Seata等);
- 运维成本上升:多个数据库意味着更多的实例、备份、监控、迁移等工作;
垂直分库的使用场景:
- 业务系统庞大,单一数据库难以承载所有业务模块的数据和访问压力;
- 不同业务模块有着明显的数据访问差异,比如用户服务访问频繁,而配置服务访问较少;
- 希望对不同业务进行独立部署、扩展和维护
垂直分表
垂直分表是指将一张表中字段(列)按照访问频率、数据业务大小或业务用途,拆分成多张表,但每张表都对应相同的行(即一条记录的数据被分散到不同的表中,但通过主键关联)

垂直分表是针对业务上字段较多的大表进行的,一般是业务宽表中比较独立的字段,或者不常用的字段拆分到单独的数据表中,是一种大表拆小表的模式。
垂直分表的优点:
- 提升查询性能:减少每次查询需要读取的字段数量,特别是避免读取大字段(如TEXT/BLOB/JSON)或冗余字段,提高查询速度和缓存命中率;
- 降低IO压力:表越瘦,每次读取的数据量越小,磁盘IO和网络传输开销更低;
- 优化索引效率:窄表更容易设计和优化索引,索引占空间小,效率高;
- 提高可维护性:表结构更清晰,字段分类明确,便于理解和维护;
- 适应不同的访问频率:高频率访问字段和低频字段访问分开存储,可以按需加载,提升系统响应速度;
垂直分表的缺点:
- 跨表查询麻烦:如果业务需要同时获取多个字段,需要联合查询或多表处哈讯,增加了SQL复杂度;也可以选择分别查询后,在代码中合并数据;
- 事务管理略微复杂:如果更新操作涉及多个拆分表,需要保证事务一致性,可能需要引入分布式事务或应用层控制;
- 开发复杂度高:业务代码中需要关注字段属于哪张表,增加了组装数据的逻辑。
垂直分表的适应场景:
- 单表字段过多(宽表):比如有几十甚至上百个字段,表非常“宽”;
- 字段访问频率差异大:有些字段经常被访问(如用户名、手机号),有些很少使用(如用户备注、个人简介);
- 存在大字段:字段占用空间大,读取会拖慢整个查询,适合拆出去;
- 希望优化查询性能和存储效率:想让常用字段查询更快,减少不必要的数据传输;
水平拆分
水平分库
将同一个数据库中的同一类数据表(如t_order表)的数据行,按照一定的规则(如用户ID哈希、范围、时间等),分散存储到多个数据库实例(不同的数据库服务/不同的物理库)中,每个数据库中都有一张结构相同的表,但只存一部分数据。

水平分库的常见拆分规则:
- 哈希取模:user_id % 4 = 0 存储到db0,user_id % 4 = 1存储到db1,以此类推;
- 优点:数据分布较为均匀;
- 缺点:扩容时迁移成本较高;
- 范围拆分:user_id 1 ~ 1000万存储到db0,1000万 ~ 2000万存储到db1,以此类推;
- 优点:容易定位数据
- 可能导致数据倾斜(新用户集中在某个范围);
- 时间范围拆分:2023年用户存储到db1,2024年用户存储到db2,以此类推;
- 常见于日志、订单等有时间属性的数据;
- 地理位置/业务单元拆分:华北用户存储在db1,华南用户存储在db2,以此类推;
- 适合有地域属性的业务;
水平分库的优点:
- 解决单库瓶颈,极大提升系统并发能力与性能:单个数据库由连接数、IO、CPU、存储上限,水平分库将这些压力分散到多个库中,整体并发能力成倍提升;
- 支持海量数据存储:单库一般有数据量限制(比如MySQL单库建议不超过几千万到亿级别),水平分库后,每个库只存储一部分的数据,可以轻松支撑更大规模的数据;
- 提高系统的扩展性:当用户量、订单量增长时,可以通过增加新的数据库实例来扩容,实现线性扩展;
- 降低单库故障的影响范围:某个库宕机,只影响部分数据,不会导致全部不可用,提高系统可用性和容灾能力;
水平分库的缺点:
- 跨库查询非常复杂:比如要查询大于30岁的用户,这些用户可能分布到不同的数据库,无法直接用一条SQL查询出来。
- 应用层分别查询再合并(复杂);
- 使用分布式数据库中间件(如ShardingJDBC、MyCat);
- 采用数据冗余/聚合表;
- 分布式事务难以保证:如果一笔业务涉及到多个数据库写入,传统事务机制无法保证原子性,必须引入分布式解决方案;
- 数据路由以及数据定位复杂:必须清除知道某条数据存储在哪个数据库,就需要设计好分片规则,并在代码或中间件中做好路由逻辑;如果规则变动,好会涉及到大规模数据迁移;
- 运维成本高:多个数据库意味着更多的实例、备份、监控、迁移、扩容等工作,系统复杂度显著上升;
- 扩容难度大:尤其是哈希取模的方式;
水平分表
将一张表中的数据(即数据行)按照某种规则(如主键ID范围、哈希值、时间等),拆分到多张结构相同的表中,每张表只存一部分数据,但表结构完全一致。

水平分表和水平分库的道理是差不多类似的,它们区别在于:
- 水平分库后,相同结构的数据表是存储在不同的数据库实例中的;
- 水平分表后,相同结构的数据表是存储在相同的数据库实例中的;
水平分表仅管拆分了表,但子表还是在同一个数据库实例中,只是解决了单表数据量过大的问题,并没有将查分后的表分到不同的机器上,还在竞争同一个物理机的CPU、内存、网络IO等。要想进一步提升性能,就需要将拆分后的表分散到不同的数据库中,达到分布式的效果。

分库分表的理论依据
当数据被拆分到了不同的地方,我们就需要知道哪些数据存储在那些地方。类似,当粮仓足够存储的时候,只需要一个粮仓,我们每次去取就直接去这个粮仓取粮即可。但是,当粮食的数量足够的大的时候,一个粮仓就不够了,就需要很多的粮仓来存储,因此我们需要有一个角色能够告诉我们哪些粮食存储在哪些粮仓。
为了便于我们查询,我们的路由表需要根据一定的规则来生成,其中常用的路由规则如下:
取模算法
对关键字段进行取模(对hash结果取余数 hash(xxx) mod N,N为数据库实例或子表的数量)算法是最为常见的一种路由方式。
以t_order订单表为例,先给数据库从0到N-1进行编号,对t_order订单表中order_no订单编号字段进行取模运算hash(order_no) mod N,得到余数为1,则存储到第一个数据库中。
采用取模运算,实现较为简单,同时对于同一个订单的数据都是落在同一个库中。查询时用相同的规则,能更快速的定位到数据。
取模运算对于集群的伸缩不太友好,主要体现在扩容之后大部分数据的取模的结果会发生变化,此时就会涉及到大量的数据迁移。
一致性哈希算法
一致性哈希算法是对于哈希算法提出的改进,它可以实现节点删除和添加只会影响一小部分数据的映射关系。传统的取模算法中,是对服务器的数量进行取模,而一致性哈希算法是对232取模,具体步骤如下:
- 一致性哈希算法将整个哈希值按照空间顺时针方向形成一个虚拟的圆环,成为Hash环;
- 接着将各个服务器使用Hash函数进行哈希,具体可以选择服务器的IP或主机名作为关键字进行哈希,从而确定每台机器在哈希环上的位置;
- 最后使用算法定位数据访问到相应服务器:将数据Key使用相同的函数Hash计算出哈希值,并确定此数据在换上的位置,从此为止沿着顺时针寻找,第一台遇到的服务器就是其应该定位到的服务器;

如果所示,我们根据 0 ~ 232 之间的点按照顺时针的方向构成一个环,这个环就称之为一致性Hash环。然后我们的数据存储由三台服务器完成,我们可以根据服务器:index=hashcode(mac_address)%232的值来确定它在Hash环的位置,如上图的服务器A、服务器B和服务器C的位置所示。
假如我们现在有四条订单数据需要写入到数据存储中,我们可以计算每条记录:index=hashcode(orderno)%232的值来确定它在Hash环中的位置,如上图的data1、data2、data3和data4所示。然后需要沿着顺时针的方式移动,将它存储在他们遇到的第一台服务器中,所以上图中的:data1和data4都是存储在服务器A中,data2存储在服务B中,data3存储在服务器C中。
同样如果我们需要查询数据的时候也可以先计算出它在Hash环中的位置,然后按照顺时针的方向进行寻找,找到的第一台服务器就是该数据的存储位置。
一致性哈希算法的扩容机制
在上面的基础上,假如我们需要新增一台服务器D,如下图所示:

依旧是按照一致性哈希算法的分配原则来看,其实只有data4存储的服务发生了改变,它原本是存储在服务器A中的数据,现在需要存储在服务器D上。也就是从服务器D到服务器C之间的数据是需要迁移到服务器D上的,并且这些需要迁移的数据都是在服务器A上。所以采用一致性哈希算法进行数据分片时,相比与普通的哈希算法,它能最大程度的减少因为扩容引起的数据迁移的范围。
范围限定法
范围限定算法是根据某些特殊字段的范围进行分片,例如根据订单数据的时间来进行分片,2023年的数据存在一个数据库中,2024年的数据存在另外一个数据库中。
采用范围限定法的优势在于实现简单,并且扩容起来也非常方便,只需要增加节点而无需进行数据迁移。缺点在于连续的分片会导致数据倾斜,例如2024年业务的飞速增长,导致2024年的订单数据量要远远比20203年和2025年多,因此会导致2024年的订单表的数量非常大,从而导致数据查询的性能也会下降。
范围+取模算法
对于单纯的范围限定法,对他的优化可以采取范围+取模的算法:
采用范围+取模算法的时候,如果某个年份的数据量特别大,我们可以在对应的库中再通过取模算法进行水平分表。这样可以保证单表数据量的大小在一个可控的范围内。
地理位置分片
地理位置分片也可以看成是范围分片,只不过他的范围比较大,采取的是地理位置,例如按照华东、华南地区进行分片。
分库分表带来的问题
分库分表的解决了大数量的存储和性能问题,但是也提高了系统的复杂度,即使一些简单的查询也需要额外的操作来保证。
分页、排序和跨节点联合查询
分页、排序和联合查询这些都是业务开发中非常常用的操作,在分库分表之后,这些简单的操作会变得复杂起来。对于分页而言,如果我们要查询的数据在多个数据库中,我们就需要先从每个数据库的实例获取到数据,然后再进行排序与合并。
事务的一致性
分库分表后不同的数据在不同的库中,因此会产生跨库事务,所以仅仅靠数据的ACID是无法保证数据的一致性的。此时,就需要额外引入其它的方式来保证数据的一致性,例如分布式事务(Seata、TCC、XA等等)。
全局唯一的主键
因为数据分别存储在不同的服务实例上,如果我们仍然是采取自增键作为记录的主键,那么服务器A和服务器B上都有可能存在id=1的这个记录,因此会出现不符合业务约束的数据。
所以分库分表通常会伴随这个分布式ID的生成,发放这个ID的系统就称之为发号器。
多数据库的高效治理
因为分库分表会产生很多的表和库,例如某宝的订单表,可能会存在成千上万个订单表,对着这些表的建立、数据量的监控以及数据的管理都是需要有一套成熟的机制的,如果全都由人工来控制的话,将会是不可估量的工作量。
历史数据的迁移
分库分表的架构落地后,首先需要解决的就是如果将原来的数据平滑的迁移到新的数据库中,采用增量的数据迁移或者是全量的数据迁移都有很多的问题需要解决。
分库分表的架构模式
在我们的分库分表的架构落地后,它主要存在两种模式:客户端模式和代理模式。
客户端模式
客户端模式就是由应用程序根据分片的规则计算数据在哪个分片上,然后跟对应的分片建立对应的连接,再从对应的分片中读取或者写入数据。
代理模式
代理模式就是原本应用需要跟对应的数据存储实例建立连接来进行数据的读写,现在全都由代理服务来实现。应用只需要连接代理服务,由代理服务与数据库实例进行连接,然后将响应应用的读写请求。
客户端模式和代理模式的区别
| 特点 | 客户端模式 | 代理模式 |
|---|---|---|
| 性能 | 直接连接数据库服务实例,性能更好 | 需要连接代理服务,连接的链路加长了,性能略低 |
| 复杂度 | 通常只需要进入一个jar,进行相关的配置即可 | 需要额外部署一个代理服务,有一定的维护成本 要求代理服务是高可能,否则会出现单点问题 |
| 升级 | 依赖于引入的jar包,一旦由版本需要升级或者Bug需要修改,所有的应用都需要进行升级 | 发布新的功能或者修复bug,只需要对代理服务进行修复即可 |
| 治理和监控 | 内嵌在应用中,在集群部署的模式下不太方便统一处理 | 在对SQL限流、读写权限的控制、监控告警等服务治理方面更加优雅。 |