type
status
date
slug
summary
tags
category
password
1、分库分表概述
1.1 分库分表是什么
分库分表是一种数据库或者数据表分割的技术,它将原本存储在单个数据库中的数据分散到多个数据库中,将原本单个表中的数据分散到多个数据表中。通过这种方式提高数据库的查询性能和扩展性,同时降低数据库的负载。
为什么要做分库分表?
分库分表是数据库架构设计中常用的优化手段,主要为了解决以下几个核心问题:
- 查询性能问题:当单表数据达到千万级甚至亿级时,表索引层级变深,查询性能会显著下降
- 数据库负载压力大:单一数据库实例的 CPU、内存、IO 等资源有限,难以支撑高并发访问
- 存储容量限制:单个数据库服务器的存储空间有限,超大规模数据无法存放在单一物理节点上
- 业务扩展需求:不同业务模块可以独立扩展(如用户库、订单库分离),便于实现微服务架构下的数据隔离
分库分表带来的好处:
- 提升查询性能:数据分散后单表数据量减少,查询更快
- 提高并发处理能力:负载分散到多个数据库实例
- 突破单机存储限制:理论上可以无限水平扩展
- 增强系统可用性:单个节点故障不影响整体服务
1.2 分库分表策略
分库分表有水平拆分和垂直拆分两种方案,这两种方式同时适用于分库和分表。
1.2.1 水平切分策略
水平拆分是指按照数据行进行拆分,将同一表的不同行分散到不同的库/表中。例如 Mysql 表的行数超过 2000 万行时就会变慢,这时可以把一张的表的数据拆成多张表来存放。水平拆分策略在后期数据量起来之后比较常用。
水平拆分策略中我们一般通过路由字段把数据路由到具体的库/表,用来路由的字段称作
shardingkey
。比如订单表按照用户 ID 作为shardingkey
,这样就可以根据用户 ID 进行查询。还可以在订单号的生成规则带上 10 位用户 ID 作为shardingkey
,落具体表的时候根据订单号中的用户 ID hash 取模,这样无论根据订单号还是用户 ID 查询效果都是一样的。
水平拆分常用拆分方式:
- 哈希分片:对 shardingkey 取模
- 范围分片:按ID范围、时间范围
- 目录分片:维护分片规则表
1、哈希分片
最常使用的方案,对
shardingkey
取模来切分。例如 user_id%2=0
即路由到 0 库, user_id%2=1
即路由到 1 库。一般表数为2^n
,hash 算法为mod2^n
,便于后期扩展。

优点:实现简单
缺点:扩容时需要迁移历史数据
2、范围分片
把
id
、user_id
、createtime
等具备范围属性的字段作为 shardingkey
,按 shardingkey
进行范围分段切割,不同段数据放到不同的库。例如 user_id
的值属于 [0, 1亿]
范围分配到 0 库,属于 [1亿, 2亿]
范围分配到 1 库。
优点:单表大小可控,天然水平扩展
缺点:无法解决集中写入瓶颈的问题
3、目录分片
将
shardingkey
和库的映射关系记录在一个单独的库中。
优点:ID 和库的 Mapping 算法可以随意更改
缺点:引入额外的单点
1.2.2 垂直拆分策略
垂直拆分是指按照列进行拆分,将不同字段分散到不同的库/表中。例如 Mysql 单库有性能瓶颈,承受的操作峰值应该在 2000 左右,超过的话应该分库,将数据拆分到不同的库中。垂直拆分一般是数据库表设计之初就执行的步骤。
垂直拆分常用场景:
- 按字段拆分:
- 把不常用的字段单独放在一张表。
- 把 text,blob 等大字段拆分出来放在附表中。
- 经常组合查询的字段放在一张表中。
- 按业务模块拆分:比如将用户信息和订单信息分别存储在不同的数据库中,实现单功能模块单库。
2、分库分表常用实现方案
2.1 客户端分片
- 实现方式:在应用层代码中实现分片逻辑
- 优点:简单直接,无额外依赖
- 缺点:侵入性强,维护成本高
- 代表:基于 Mybatis 插件,实现
Interceptor
接口,拦截 SQL 后自定义分库分表逻辑。
2.2 中间件代理
- 实现方式:使用独立代理服务处理分片逻辑
- 优点:应用透明,可集中管理
- 缺点:增加网络跳数,性能开销
- 代表:
- MySQL Router
- ProxySQL
- Atlas(360,基于 MySQL Proxy)
2.3 数据库中间件
- 实现方式:使用专门的分库分表中间件
- 优点:功能完善,支持多种分片策略
- 缺点:学习成本,运维复杂度
- 代表:
- ShardingSphere(Apache开源,含 Sharding-JDBC、Sharding-Proxy)
- MyCat(基于阿里 Cobar 开发)
- TDDL(淘宝分布式数据层)
2.4 分布式数据库
- 实现方式:使用原生支持分片的分布式数据库
- 优点:天然支持,无需额外开发
- 缺点:技术锁定,迁移成本
- 代表:
- TiDB
- CockroachDB
- OceanBase
2.5 技术选型
- 中小规模:Sharding-JDBC(应用层集成)
- 大规模复杂场景:Sharding-Proxy 或 MyCat
- 全新项目:考虑 TiDB 等分布式数据库
- 云环境:优先考虑云厂商提供的分布式数据库服务
3、分库分表的实践过程
3.1 前期准备与评估阶段
- 业务需求分析
- 确定分库分表的必要性:通常单表数据量超过 5000 万行或单库 QPS 超过 5000 时考虑
- 分析业务特点:读写比例、查询模式、事务要求等
- 容量评估
- 评估当前数据量及增长速度
- 计算未来3-5年的数据量预期
- 评估存储资源需求
3.2 设计与规划阶段
- 技术选型
- 选择分库分表中间件:ShardingSphere、MyCat等
- 确定数据同步工具:Canal、DataX等
- 分片策略设计
- 确定分库策略:使用垂直拆分,按业务模块拆分
- 确定分表策略:使用水平拆分,使用哈希取模拆分。
- 新表结构设计
- 合并冗余表,剔除无用字段
- 重新设计索引,避免照搬原表索引
- 尽量保持原字段名称,减少业务改造
- 架构设计
- 设计数据同步关系:全量同步 + 增量同步
3.3 改造和实施阶段
- 应用改造
- 提供新库的数据访问层:通过 SDK 形式提供新库的数据访问层,基于 Sharding-JDBC,支持分片路由。
- 双写改造:在服务层对订单表进行增删改的地方,需要修改相应的代码同时写新库和老库,代码需要提供灰度功能以及热开关切换功能。
- 分布式 ID 问题:引入雪花算法解决分布式 ID 生成问题
- 分布式事务问题:改造事务处理,引入分布式事务框架(Seata)或最终一致性方案
- 制定数据迁移方案
- 全量迁移:使用 DataX 完成旧数据全量迁移到新的数据库
- 增量同步:使用 Canal 监听增量变更的 Binlog 数据
- 数据校验程序:用于校验新库和老库的数据是否一致,对比 MD5 校验
3.4 上线与切换阶段
不停机数据迁移流程:
- 开启双写。任何对数据库的更改操作(增/删/改)都要同时写入老库和新库。如果新库没有相关记录,需要先从老库查出记录,将更新后的记录写入新库。为了保证写入性能,老库写完后,可以采用消息队列异步写入新库。
- 开启旧数据全量迁移:将某一时间戳之前的老数据迁移到新库。
- 时间戳一定要选择第 2 步开启双写后的时间点,比如开启双写后 10 分钟的时间点,避免部分老数据被漏掉。
- 迁移过程遇到主键冲突直接忽略,因为第 2 步的更新操作,已经把记录拉到了新库。
- 迁移过程一定要记录日志,尤其是错误日志,如果有双写失败的情况,我们可以通过日志恢复数据,以此来保证新老库的数据一致。
- 开启校验数据脚本:等待第 3 步完成后,开启校验程序脚本,校验新库数据的完整性和准确性。
- 流量切换,开启双读:数据校验没问题后,开启双读,起初使用灰度功能给新库放少部分流量,新库和老库同时读取。由于延时问题,新库和老库可能会有少量数据记录不一致的情况,所以新库读不到时需要再读一遍老库,期间遇到问题可以及时把读流量切回老库。
- 停止老库读流量:逐渐提高新库的读流量比例,逐渐将全部读流量切到新库。
- 停止老库写流量:读流量全部切到新库后,关闭老库写入,只写新库。
- 迁移完成:此时老库已经没有读写流量,后续可以考虑回收老库资源,并去掉双写双读相关无用代码。
3.5 后期优化与运维
- 监控体系建设
- 监控分片存储空间使用率、跨分片查询比例等指标
- 设置报警阈值,如存储 >80%、跨分片查询 >5% 等
- 性能优化
- 处理热点分片:动态调整路由策略
- 查询优化:强制指定分片键
- 引入缓存减轻数据库压力
- 扩容准备
- 设计时考虑未来扩容,如使用
2^n
分片数 - 准备数据迁移和路由调整方案
4、分库分表带来的挑战
分库分表的技术难点:
- 跨库JOIN问题
- 非shardingkey的查询问题
- 分页排序问题
- 分布式唯一主键
- 数据迁移
- 跨库事务
4.1 跨库JOIN问题
在分库分表架构中,因为数据被分散在不同的数据库或表中,传统的 SQL Join 操作无法直接使用,这就是跨库 JOIN 问题。
尽量避免跨库 Join
在设计时优先考虑单库操作,让需要 Join 的数据尽量在同一个分片。如果一定要实现跨库 JOIN,目前常见的技术方案有:
技术方案 | 实现 | 优点 | 缺点 |
应用层Join | 先查询一个表的数据,根据结果再去查询另一个表,在应用内存中完成数据关联 | • 实现简单直接
• 不需要中间件支持 | • 需要多次查询,性能较差
• 网络开销大
• 应用代码复杂度高 |
全局表/广播表 | 将需要频繁Join的小表在所有分库中都保存一份,这些表数据通常很少变更 | • Join操作可以在单个库内完成
• 性能较好 | • 只适用于小表
• 数据同步有延迟问题 |
字段冗余 | 在表中冗余存储需要Join的字段,避免Join操作 | • 查询性能好
• 实现简单 | • 数据一致性维护困难
• 存储空间增加 |
使用中间件 | 使用ShardingSphere、MyCat等中间件,解析SQL并重写查询 | • 对应用透明
• 可以支持部分跨库Join | • 性能开销
• 复杂Join可能不支持 |
分布式查询引擎 | 使用Presto、Doris等分布式查询引擎从各分片获取数据并合并 | • 支持复杂查询
• 对应用透明 | • 系统复杂度高
• 资源消耗大 |
4.2 非shardingkey的查询问题
一般情况下我们使用的
sharding key
都是针对 C 端的用户,这是数据库的主要流量。而非 shardingkey
一般都是针对B端用户的,例如来自商户端或者后台的查询,这种查询流量不大而且可以接受少量延迟,一般解决方案有:1、双写。双写就是下单的数据落两份,C 端和 B 端的各自保存一份,C 端用你可以用单号、用户 ID 做
shardingkey
都行,B端就用商家卖家的 ID 作为 shardingkey
就好了。对于 B 端来说轻微的延迟是可以接受的,所以可以采取异步的方式去落 B 端订单。
2、走离线数仓或者ES查询。订单数据落库之后,不管你通过 Binlog 还是 MQ 消息的都形式,把数据同步到数仓或者 ES,他们支持的数量级对于这种查询条件来说就很简单了。同样这种方式肯定是稍微有延迟的,但是这种可控范围的延迟是可以接受的。这种解决方案还可以解决比如运营、业务、产品等复杂的查询条件。

4.3 分页排序问题
分页排序要解决的问题是
跨越多个水平切分数据库,且分库依据与排序依据为不同属性,并需要进行分页,实现select * from T order by time offset X limit Y
的跨库分页SQL
这个问题目前仍是业界难题,常用有以下解决方案:
4.3.1 全局视野法
实现方案:
- 将
order by time offset X limit Y
,改写成order by time offset 0 limit X+Y
- 服务层将改写后的 SQL 语句发往各个分库
- 假设共分为 N 个库,服务层将得到 N*(X+Y) 条数据
- 服务层对得到的 N*(X+Y) 条数据进行内存排序,内存排序后再取偏移量 X 后的 Y 条记录,就是全局视野所需的一页数据
优点:
- 通过服务层修改SQL语句,扩大数据召回量,能够得到全局视野,业务无损,精准返回所需数据。
缺点:
- 网络消耗增加:每个分库需要返回更多的数据,增大了网络传输量(耗网络);
- CPU 消耗增加:除了数据库按照 time 进行排序,服务层还需要进行二次排序,增大了服务层的计算量(耗CPU);
- 性能下降明显:这个算法随着页码的增大,性能会急剧下降,这是因为 SQL 改写后每个分库要返回 X+Y 行数据。假如要返回第 100页,offset 中的 X=9900,即每个分库要返回 100 页数据,数据量和排序量都将大增,性能平方级下降。
4.3.2 业务折衷法-禁止跳页查询
为了解决全局视野法的性能问题,很多产品并不提供“直接跳到指定页面”的功能,而只提供“下一页”的功能。这种设计可以大大降低难度:
- 用全局视野法取得第一页数据,并得到第一页记录的
time_max
。
- 每次翻页,将
order by time offset X limit Y
,改写成order by time where time > $time_max limit Y
,以保证每次每个库只返回一页数据,性能为常量。
优点:
- 数据的传输量和排序的数据量不会随着不断翻页而导致性能下降。
缺点:
- 需要业务配合
4.3.3 业务折衷法-允许模糊数据
原理是:使用 shardingkey 进行分库,在数据量较大,数据分布足够随机的情况下,各分库所有非 patition key 属性,在各个分库上的数据分布,统计概率情况是一致的。
在允许数据精度损失的前提下。利用这一原理:
- 将
order by time offset X limit Y
,改写成order by time offset X/N limit Y/N
- 每个库返回的都是精度不够准确的数据。
优点:
- 大大降低了技术方案的复杂度,既不需要返回更多的数据,也不需要进行服务内存排序了。
缺点:
- 返回的数据不够准确
4.3.4 二次查询法
这个方法要求数据均摊到各分表,就是说不会有一个分表的数据特别多或特别少。这个方案不适用数据分段切分的表。实现思路:
- 将
order by time offset X limit Y
,改写成order by time offset X/N limit Y
- 找到最小值
time_min
- between 二次查询,
order by time between $time_min and $time_i_max
,第二次查询条件放宽了,故第二次查询会返回比第一次查询结果集更多的数据
- 找到
time_min
在各个分库的 offset,从而得到time_min
在全局的 offset
- 得到了
time_min
在全局的 offset,自然得到了全局的offset X limit Y
优点:
- 即能够满足业务的精确需要,无需业务折衷,又高性能
缺点:
- 需要二次查找数据库
4.4 分布式唯一主键
详见 分布式ID:实现汇总
4.5 数据迁移
详见
3.3 改造和实施阶段
3.4 上线与切换阶段
4.6 跨库事务
常规的数据库事务都是在单个连接(Session) 里面实现,如果要跨库,就要涉及多个数据库连接,常规的数据库事务不支持这种场景,这就是跨库事务问题。
尽量避免跨库事务
数据设计时尽量让需要事务的操作在同一个分片,避免在一个事务中修改 db0 中的表的时候同时修改 db1 中的表。
如果一定要实现跨库事务,就需要使用分布式事务框架,参考这一篇文章 https://www.mcbilla.com/article/86e9cb68-7041-4732-a44c-3385aec608cd
- Author:mcbilla
- URL:http://mcbilla.com/article/2cd55850-e1fc-4303-978f-ee4659c0b36d
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!
Relate Posts