type
status
date
slug
summary
tags
category
icon
password
1、分库分表概述
分库分表是一种数据库或者数据表分割的技术,它将原本存储在单个数据库中的数据分散到多个数据库中,将原本单个表中的数据分散到多个数据表中。通过这种方式提高数据库的查询性能和扩展性,同时降低数据库的负载。
分库分表一般有水平拆分和垂直拆分两种方案。
1.1 水平切分策略
水平拆分是指按照数据行进行拆分,将同一表的不同行分散到不同的库/表中。例如 Mysql 表的行数超过 2000 万行时就会变慢,这时可以把一张的表的数据拆成多张表来存放。水平拆分策略在后期数据量起来之后比较常用。
水平拆分策略中我们一般通过路由字段把数据路由到具体的库/表,用来路由的字段称作
shardingkey
。比如订单表按照用户 ID 作为shardingkey
,这样就可以根据用户 ID 进行查询。还可以在订单号的生成规则带上 10 位用户 ID 作为shardingkey
,落具体表的时候根据订单号中的用户 ID hash 取模,这样无论根据订单号还是用户 ID 查询效果都是一样的。
水平拆分常用拆分方式:
- 哈希分片(对 shardingkey 取模)
- 范围分片(按ID范围、时间范围)
- 目录分片(维护分片规则表)
2.1.1 哈希分片
最常使用的方案,对
shardingkey
取模来切分。例如 user_id%2=0
即路由到 0 库, user_id%2=1
即路由到 1 库。一般表数为2^n
,hash 算法为mod2^n
,便于后期扩展。

优点:实现简单
缺点:扩容时需要迁移历史数据
2.1.2 范围分片
把
id
、user_id
、createtime
等具备范围属性的字段作为 shardingkey
,按 shardingkey
进行范围分段切割,不同段数据放到不同的库。例如 user_id
的值属于 [0, 1亿]
范围分配到 0 库,属于 [1亿, 2亿]
范围分配到 1 库。
优点:单表大小可控,天然水平扩展
缺点:无法解决集中写入瓶颈的问题
2.1.3 目录分片
将
shardingkey
和库的映射关系记录在一个单独的库中。
优点:ID 和库的 Mapping 算法可以随意更改
缺点:引入额外的单点
- 垂直分表:指数据表列的拆分,把一张列比较多的表拆分为多张表,垂直分表一般是数据表设计之初就执行的步骤。例如:
- 把不常用的字段单独放在一张表;
- 把text,blob等大字段拆分出来放在附表中;
- 经常组合查询的列放在一张表中。
- 水平分表:指数据表行的拆分,
1.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、分库分表带来的挑战
分库分表的技术难点:
- 跨库JOIN问题
- 非shardingkey的查询问题
- 分页排序问题
- 分布式唯一主键
- 数据迁移
- 跨库事务
3.1 跨库JOIN问题
在分库分表架构中,因为数据被分散在不同的数据库或表中,传统的 SQL Join 操作无法直接使用,这就是跨库 JOIN 问题。
尽量避免跨库 Join
在设计时优先考虑单库操作,让需要 Join 的数据尽量在同一个分片。如果一定要实现跨库 JOIN,目前常见的技术方案有:
技术方案 | 实现 | 优点 | 缺点 |
应用层Join | 先查询一个表的数据,根据结果再去查询另一个表,在应用内存中完成数据关联 | • 实现简单直接
• 不需要中间件支持 | • 需要多次查询,性能较差
• 网络开销大
• 应用代码复杂度高 |
全局表/广播表 | 将需要频繁Join的小表在所有分库中都保存一份,这些表数据通常很少变更 | • Join操作可以在单个库内完成
• 性能较好 | • 只适用于小表
• 数据同步有延迟问题 |
字段冗余 | 在表中冗余存储需要Join的字段,避免Join操作 | • 查询性能好
• 实现简单 | • 数据一致性维护困难
• 存储空间增加 |
使用中间件 | 使用ShardingSphere、MyCat等中间件,解析SQL并重写查询 | • 对应用透明
• 可以支持部分跨库Join | • 性能开销
• 复杂Join可能不支持 |
分布式查询引擎 | 使用Presto、Doris等分布式查询引擎从各分片获取数据并合并 | • 支持复杂查询
• 对应用透明 | • 系统复杂度高
• 资源消耗大 |
3.2 非shardingkey的查询问题
一般情况下我们使用的
sharding key
都是针对 C 端的用户,这是数据库的主要流量。而非 shardingkey
一般都是针对B端用户的,例如来自商户端或者后台的查询,这种查询流量不大而且可以接受少量延迟,一般解决方案有:1、双写。双写就是下单的数据落两份,C 端和 B 端的各自保存一份,C 端用你可以用单号、用户 ID 做
shardingkey
都行,B端就用商家卖家的 ID 作为 shardingkey
就好了。对于 B 端来说轻微的延迟是可以接受的,所以可以采取异步的方式去落 B 端订单。
2、走离线数仓或者ES查询。订单数据落库之后,不管你通过 binlog 还是 MQ 消息的都形式,把数据同步到数仓或者ES,他们支持的数量级对于这种查询条件来说就很简单了。同样这种方式肯定是稍微有延迟的,但是这种可控范围的延迟是可以接受的。这种解决方案还可以解决比如运营、业务、产品等复杂的查询条件。

3.3 分页排序问题
分页排序要解决的问题是
跨越多个水平切分数据库,且分库依据与排序依据为不同属性,并需要进行分页,实现select * from T order by time offset X limit Y
的跨库分页SQL
这个问题目前仍是业界难题,常用有以下解决方案:
3.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 页数据,数据量和排序量都将大增,性能平方级下降。
3.3.2 业务折衷法-禁止跳页查询
为了解决全局视野法的性能问题,很多产品并不提供“直接跳到指定页面”的功能,而只提供“下一页”的功能。这种设计可以大大降低难度:
- 用全局视野法取得第一页数据,并得到第一页记录的
time_max
。
- 每次翻页,将
order by time offset X limit Y
,改写成order by time where time > $time_max limit Y
,以保证每次每个库只返回一页数据,性能为常量。
优点:
- 数据的传输量和排序的数据量不会随着不断翻页而导致性能下降。
缺点:
- 需要业务配合
3.3.3 业务折衷法-允许模糊数据
原理是:使用 shardingkey 进行分库,在数据量较大,数据分布足够随机的情况下,各分库所有非 patition key 属性,在各个分库上的数据分布,统计概率情况是一致的。
在允许数据精度损失的前提下。利用这一原理:
- 将
order by time offset X limit Y
,改写成order by time offset X/N limit Y/N
- 每个库返回的都是精度不够准确的数据。
优点:
- 大大降低了技术方案的复杂度,既不需要返回更多的数据,也不需要进行服务内存排序了。
缺点:
- 返回的数据不够准确
3.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
优点:
- 即能够满足业务的精确需要,无需业务折衷,又高性能
缺点:
- 需要二次查找数据库
3.4 分布式唯一主键
在分库分表后如何生成全局唯一的数据库主键,常见有以下解决方案:
3.4.1 业务字段组合唯一键
实现:使用业务字段(如用户ID、时间戳等)组合作为唯一标识,例如
yyMMddHHmm[10] + uid后四位[4] + server Id[2] + 随机数[4]
优点:无需额外组件
缺点:依赖业务规则,可能不够通用
3.4.2 UUID
实现:UUID(Universally Unique Identifier,通用唯一标识符)是一种用于唯一标识信息的标准化格式,通常由 32个十六进制数字 组成,以连字符分隔为五组,形式为:
8-4-4-4-12(例如:
550e8400-e29b-41d4-a716-446655440000
)。Java 内置了 UUID 的生成工具。优点:简单易实现,全局唯一
缺点:无序导致索引效率低,存储空间大(128位)
适用场景:对性能要求不高的小型系统
3.4.3 数据库自增
实现:单独部署一个数据库实例专门用于生成ID,创建一张用于生成 ID 的数据表,给 id 字段设置
auto_increment_increment
和 auto_increment_offset
来保证 ID 自增,每次业务使用下列 SQL 读写 MySQL 得到ID号。流程如下所示

优点:简单易实现
缺点:
- 强依赖 Mysql,当 Mysql 异常时整个系统不可用。
- 受 Mysql 的读写性能限制,容易出现性能瓶颈。
3.4.4 号段模式
号段模式是数据库自增方案的优化。
实现:预先从数据库获取一批 ID 号段,缓存在本地内存中,当应用需要 ID 时直接从内存中获取,当号段使用完毕后再向数据库申请新的号段。通常需要设计一个专门的表来管理号段:
实现步骤:
- 应用启动时或当前号段用完时,向数据库申请新号段
- 数据库通过原子操作更新 max_id 并返回新号段范围
- 应用将号段范围缓存在内存中
- 应用从内存中顺序分配ID
优点
- 高性能:ID生成在内存中完成,性能极高
- 可扩展:通过增加step可以调整号段大小,适应不同吞吐量需求
- 简单可靠:实现简单,依赖少,稳定性高
- 趋势递增:生成的ID是趋势递增的,适合数据库索引
缺点:
- ID不连续:号段之间不连续,但趋势递增
- 重启可能浪费:应用重启可能导致号段中的ID未被使用而浪费
- 数据库依赖:仍然依赖数据库,但访问频率很低
优化方案
- 双Buffer优化:预加载下一个号段,实现无缝切换(美团 Leaf 的实现方案)
- 动态调整step:根据使用情况动态调整号段大小
- 多实例隔离:不同业务使用不同 biz_tag 隔离
- 监控告警:监控号段使用情况,提前预警
3.4.5 snowflake算法
如果对性能要求较高,可以考虑 snowflake 算法方案,64-bit 分别划分成 4段 1-41-10-12 的 64 位字符串
- 1bit不用
- 41-bit:时间戳,可以表示69年的时间
- 10-bit:workerID,可以表示1024台机器,也可以5-bit给IDC,分5-bit给工作机器。这样就可以表示32个IDC,每个IDC下可以有32台机器
- 12-bit:自增序列号

优点:
- 趋势递增,高性能
- 不依赖第三方库
缺点:
- 依赖机器时钟,时钟回拨会导致问题
3.4.6 分布式ID生成服务
业界常用的开源框架有:
- 美团Leaf:提供号段模式和snowflake算法两种实现。
- 滴滴Tinyid:基于美团 Leaf-segment 算法的扩展。
- 百度(uid-generator):基于 snowflake 算法的扩展。
以 美团Leaf 为例。分为 Leaf-segment 和 Leaf-snowflake 模式。
- Leaf-segment:对号段模式方案的优化
- segment 分段取id,减少 mysql 的 IO 次数
- 双buffer:号码消耗到一定程度(10%)就开启异步线程去取新的id

- Leaf-snowflake:对 snowflake 算法的优化,依赖 zk 生成 workerID

优点:
- 高可用
- 解决了时钟回拨的问题
缺点:
- 依赖 zookeeper
3.5 数据迁移
- 前提准备。
- 代码改成双写。在服务层对订单表进行增删改的地方,需要修改相应的代码同时写新库和老库,代码需要提供灰度功能以及热开关切换功能。
- 准备一个空白的新库。
- 准备数据迁移脚本,用于旧数据全量迁移。
- 准备校验程序脚本,用于校验新库和老库的数据是否一致。
- 开启双写,老库和新库同时写入。任何对数据库的更改操作(增/删/改)都要双写。如果新库没有相关记录,需要先从老库查出记录,将更新后的记录写入新库。为了保证写入性能,老库写完后,可以采用消息队列异步写入新库。
- 开启数据迁移脚本,将某一时间戳之前的老数据迁移到新库。
- 时间戳一定要选择第 2 步开启双写后的时间点,比如开启双写后 10 分钟的时间点,避免部分老数据被漏掉。
- 迁移过程遇到主键冲突直接忽略,因为第 2 步的更新操作,已经把记录拉到了新库。
- 迁移过程一定要记录日志,尤其是错误日志,如果有双写失败的情况,我们可以通过日志恢复数据,以此来保证新老库的数据一致。
- 等待第 3 步完成后,开启校验程序脚本,校验新库数据的完整性和准确性。
- 数据校验没问题后,开启双读,起初使用灰度功能给新库放少部分流量,新库和老库同时读取。由于延时问题,新库和老库可能会有少量数据记录不一致的情况,所以新库读不到时需要再读一遍老库。
- 提高灰度比例,逐渐将读流量切到新库。遇到问题可以及时把流量切回老库。
- 读流量全部切到新库后,关闭老库写入,只写新库。
- 迁移完成,后续可以去掉双写双读相关无用代码。

3.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