江河分流:分库分表之基础

序言

  在业务发展的早期,一个 MySQL 数据库通常就可以满足系统的需求。数据量增加了,可以优化 SQL、增加索引;读请求增加了,可以通过主从复制和读写分离分摊压力。

  但是,这些方案并不能无限解决问题。

  当单表数据量越来越大,单库的 CPU、内存、磁盘、IO 等资源逐渐达到瓶颈时,就需要考虑从数据库架构层面进行拆分,也就是分库分表

  分库分表本身并不复杂,真正复杂的是拆分之后的问题。

  数据应该按照什么策略拆分?主键如何保证全局唯一?事务如何处理?SQL 如何路由?跨库 JOIN 和分页怎么办?一个已经运行多年的系统,又应该如何完成数据迁移和流量切换?

  因此,分库分表并不是简单地将“一张表变成多张表”,而是一项完整的数据库架构改造。

  本文将从分库分表的基本概念开始,介绍分库分表的拆分方式、适用场景、分片策略、ShardingSphere-JDBC 的使用,以及生产环境中的完整拆分流程。

简介

  分库分表,简单来说就是当单个 MySQL 数据库或者单张数据表无法继续满足业务的数据量和性能要求时,将原本集中存储的数据按照一定规则拆分到多个数据库、多个数据表中。

  这里实际上包含两个概念:分库分表

拆分粒度

分库

  分库指的是在表数量不变的情况下对数据库进行拆分。

  即一个库变多个库。

  例如,原来系统只有一个 business_db,存放了以下表:

  • user
  • order
  • product
  • workflow

  随着业务发展,这个数据库中的表越来越多,同时订单、商品等核心业务产生了大量读写请求,导致整个数据库的 CPU、内存、磁盘 IO 等资源压力越来越大。

  此时可以按照业务进行垂直拆分:

  • user_db库:user
  • order_db库:order
  • product_db库:product
  • workflow_db库:workflow

  原来一个数据库存放多个业务表,拆分后变为多个数据库存放不同业务表。

  所以,分库主要解决的是单个数据库压力过大的问题,通过增加数据库实例,将原本集中在一个数据库上的压力分散到多个数据库。

分表

  分表指的是在数据库数量不变的情況下,对数据库里面的表进行拆分,即一个表变多个表。

  例如,电商系统有一张订单表order

  随着业务发展,这张表最终积累了数亿甚至几十亿条订单数据。即使 SQL 使用了合适的索引,单表数据量过大仍然可能带来索引维护、查询、更新、备份等方面的压力。

  此时可以将一张 order 表按照 user_id(user_id % 100)拆成多张:

  • 0 → order_00
  • 1 → order_01
  • 99 → order_99

  这样,原本一张order表 10 亿条数据,变为:

  • order_00 → 约 1000 万
  • order_01 → 约 1000 万
  • order_02 → 约 1000 万
  • order_99 → 约 1000 万

  这里数据库数量并没有增加,仍然可以是同一个 MySQL 实例,只是把一张非常大的表拆成了多张较小的表

分库加分表

  如果单纯分表之后,单个数据库仍然承受很大的压力,那么还可以进一步进行分库 + 分表。

  例如原来order_db库的order表存放了 20 亿条数据,可以拆成:

1
2
3
4
5
6
7
8
9
10
11
12
13
db_00
├── order_00
├── order_01
├── ...
└── order_63

db_01
├── order_64
├── order_65
├── ...
└── order_127

...

  也就是说一个数据库的一个表数据,分散到多个数据库的多张表中,最终将数据和访问压力同时分散到多个数据库、多个数据表中。

拆分方向

  当前主要的拆分方式有两种:

  • 水平拆分:水平拆分就是从左往右横着切
  • 垂直拆分:垂直拆分就是从上往下竖着切

水平拆分(常用)

  水平拆分指的是在整个表数据结构不发生变更的情況下,我们将一张表的数据拆分成多张表。

垂直拆分

  垂直拆分指的是将本来放在一张表中的字段拆分到多张表中。

  比如某张表的字段太大,影响到了此表查询的性能,那么我们可以将造成此问题的字段拆分出去。

分片算法

  假设现在有一张订单表字段结构如下:

1
2
3
4
5
6
order
├── id
├── user_id
├── shop_id
├── create_time
└── amount

  现在这张表有几十亿数据,需要拆成 100 张表。

  问题来了:这几十亿条数据,到底应该按照什么规则放进这 100 张表?

  这就是分片算法。

  例如order

  • 可以按照 user_id 分片(user_id % 100):
    • order_00
    • order_01
    • order_02
  • 可以按照 create_time 分片:
    • 2026年1月 → order_202601
    • 2026年2月 → order_202602
    • 2026年3月 → order_202603

  本质都是在回答:在不同的分片算法下,一条数据会去不同的一个分片。

  那么,实际生产环境中,面对不同的业务场景,应该选择怎样的分片规则呢?

常见的分片算法

  生产环境比较常见的可以归纳为:

分片算法 核心思想 典型分片键 主要特点
Hash 切分 对分片键做 Hash/取模 user_id、shop_id 数据均匀
范围切分 按数据范围划分 id、user_id 扩容、冷热管理方便
时间切分 按时间范围划分 create_time 非常适合订单、日志、流水
映射表切分 建立“数据 → 分片”的映射 任意业务字段 灵活,但增加维护成本
一致性 Hash Hash 环分片 user_id 等 扩容时减少数据迁移

  其中实际项目里最常见的还是:Hash、范围、时间。

  下面分别来介绍下。

Hash 切分

  例如有 100 张表:

1
2
3
4
5
order_000
order_001
order_002
...
order_099

  按照 user_idtable_index = user_id % 100,例如:

1
2
3
4
5
6
7
user_id = 1001

1001 % 100 = 1



order_001

  另一个用户:

1
2
3
4
5
6
7
user_id = 2034

2034 % 100 = 34



order_034
优点

  最大的优点就是:数据比较容易均匀分布,不会出现某几张表特别大。

  例如:

1
2
3
4
order_000 → 1000万
order_001 → 1002万
order_002 → 998万
...
缺点

  问题也比较明显:普通 Hash 取模最大的一个问题就是扩容时的数据迁移。

  如果以后从 100 张表扩容成 200 张表,原来的 user_id % 100 变成:user_id % 200,那么大量数据的路由结果都会发生变化。

  例如:

1
2
3
4
5
6
7
8
1001 % 100 = 1
1001 % 200 = 1

1234 % 100 = 34
1234 % 200 = 34

5678 % 100 = 78
5678 % 200 = 78

  有些碰巧不变,但大量数据都会重新分布。

范围切分

  范围切分就更加直观。

  例如按照订单 ID:

1
2
3
order_000:0 ~ 99999999
order_001:100000000 ~ 199999999
order_002:200000000 ~ 299999999

  那么:

  • order_id = 12345678order_000
  • order_id = 123456789order_001
优点

  最大的优点是:范围非常明确。

  比如我要查询WHERE id BETWEEN 100000000 AND 150000000,很容易知道数据在哪些分片。

  另外,后续扩容也比较自然:

  • order_000 → 0 ~ 1亿
  • order_001 → 1亿 ~ 2亿
  • order_002 → 2亿 ~ 3亿
  • 新增:order_003 → 3亿 ~ 4亿

  不需要像普通 Hash 那样重新计算所有历史数据。

缺点:热点问题

  例如订单 ID 是不断递增的,比如今天:

  • order_002 → 2亿 ~ 3亿
  • order_003 → 3亿 ~ 4亿

  那么,新订单全部进入最新的分片,于是:order_003被大量写入,成为热点,所以存在明显的写偏移。

  因此,范围切分特别需要注意热点分片

特殊范围——时间拆分

  时间切分本质上是范围切分的一种特殊形式,因为时间本身就是一种范围,所以可以把它理解成:日期切分 = 按时间字段进行范围切分。

  例如订单表order按照create_time进行切分,那么不同日期的数据到不同的表中:

  • 2026-01-01 ~ 2026-01-31order_202601
  • 2026-02-01 ~ 2026-02-28order_202602
  • 2026-03-01 ~ 2026-03-31order_202603
  • 2026-04-01 ~ 2026-04-30order_202604
日期切分适合多数业务

  由于很多业务数据天然具有明显的时间特征。

  例如:

  • 订单
  • 支付流水
  • 交易记录
  • 操作日志
  • GPS 轨迹
  • 消息记录
  • 统计数据
  • 用户行为记录

  例如你之前提到的环卫工人 GPS 轨迹就是非常典型的场景。

  假设gps_track每天产生几千万条数据,那么完全可以每天一个轨迹表:

  • gps_track_20260901
  • gps_track_20260902
  • gps_track_20260903

  当需要查询 2026-09-03 的轨迹数据时,直接去gps_track_20260903表即可。

  每天产生几千万条数据。而不是:查询几十亿数据 → 单表过滤create_time索引,这样可以天然控制单表规模。

日期切分最大优势:冷热数据管理

  这其实是日期切分非常重要的一个价值。

  例如订单:

  • 最近 3 个月 → 热数据
  • 3个月 ~ 2年 → 温数据
  • 2年以上 → 冷数据

  那么数据库可以设计成:

1
2
3
4
5
6
7
8
9
10
MySQL
├── 2026-09
├── 2026-08
├── 2026-07
└── ...

归档存储
├── 2025
├── 2024
└── ...

  甚至可以进一步:

1
2
3
4
5
6
7
在线 MySQL

最近几个月订单

历史数据库 / 对象存储 / ES

几年前订单

  比如支付宝一年前订单单独处理,就是按照数据生命周期进行冷热分离

日期切分也有一个明显问题:热点

  时间切分并不是没有问题。

  假设order_20260904是今天的订单表,那么今天请求的所有新订单数据都会写到这一张表,于是就存在明显的写热点。问题:

  • order_20260901 → 基本不写
  • order_20260902 → 基本不写
  • order_20260903 → 少量写
  • order_20260904 → 大量写

  所以在数据量非常大的场景下,往往不会简单地日期 → 一张表,而是进一步组合:日期 + Hash,例如:

1
2
3
4
5
6
2026-09-04

├── user_id % 16 = 0 → order_20260904_00
├── user_id % 16 = 1 → order_20260904_01
├── user_id % 16 = 2 → order_20260904_02
└── ...

  最终:

1
2
3
4
5
order_20260904_00
order_20260904_01
order_20260904_02
...
order_20260904_15

  这就是:时间范围 + Hash 的复合切分。

  这种方案在大规模流水、日志、轨迹类数据中很有价值。

映射表切分

  映射表与 Hash 不同,它不是通过:user_id % 100,直接计算,而是维护一张映射关系:

1
2
3
4
5
user_id    → database/table
--------------------------------
10001 → db_01.order_03
10002 → db_02.order_07
10003 → db_01.order_09

  查询的时候:user_id → 查询映射关系 → 找到具体分片 → 查询对应数据库

优点

  非常灵活。

  例如某个大客户数据量特别大,可以单独给它一个分片:

  • 普通客户 → order_00 ~ order_99
  • 大客户A → order_100
  • 大客户B → order_101
缺点

  需要维护额外的映射关系,所以它更适合:数据分布不规则、需要高度灵活控制分片位置的场景。

一致性 Hash(了解)

  普通 Hashhash(key) % N存在一个问题,当N = 10变成N = 20,则大量数据的映射都会改变。

  一致性 Hash 的核心思想是:扩容时尽量只迁移少量数据,而不是重新打乱全部数据。

  所以它主要解决的是:分片节点变化时,如何减少数据迁移。

  不过数据库分库分表场景里,实际方案往往还要结合业务特点,不是用了“一致性 Hash”就自动解决扩容问题。

小结

策略 典型分片键 优点 缺点 典型场景
Hash user_idshop_id 数据分布均匀 扩容迁移复杂 用户、门店、订单
范围 id 查询范围明确,扩容自然 容易产生热点 ID、金额、地区
时间 create_time 冷热分离、归档方便 当前分片容易成为热点 订单、流水、日志、轨迹
映射表 任意业务字段 非常灵活 需要维护映射关系 特殊客户、数据分布不均
一致性 Hash user_id 扩容迁移量较小 实现复杂 节点动态变化场景

什么时候需要分库分表?

  总体来说:性能出现瓶颈,并且其他优化手段无法很好的解决,比如:

  • 单库出现瓶颈:
  • 单表数据量较大,导致读写性能较慢
  • 网络带宽不足,导致读写性能较慢
  • 磁盘空间不足,导致无法正常写入数据
  • CPU压力过大(busy、load过高),导致读写性能较慢
  • 内存不足(缓存池命中率较低、磁盘读写 IO QPS 过高),导致读写性能较慢

分库还是分表?

  根据不同维度选择合适的策略:

  • 只分表
    • 单表数据量较大,单表读写性能出现瓶颈
    • 经过评估单库的容量和性能可以支撑未来几年的增长
  • 只分库
    • 数据库(读)写压力较大,数据库出现存储性能瓶颈
  • 分库分表
    • 单表数据量较大,单表读写性能出现瓶颈
    • 数据库(读)写压力较大,数据库出现存储性能瓶颈

  并可结合以下指标进行评估:

  • 预估数据量:建议预估三年内单表数据量大于 500W 或者单表数据文件大于 2G 就需要考虑分库分表
  • 预估数据趋势:订单这类持续高速增长的数据需尽早考虑分库分表,并且要预留空间。用户这类后期增长会放缓的数据,可以延后考虑分库分表
  • 预估应用场景:对于分片键变更频繁的数据,由于频繁变更分片键,需要同时做数据迁移,此时则不适合进行分库分表
  • 预估业务复杂度:业务逻辑与分片逻辑绑定,会给 SQL 执行带来很多限制。所以如果对数据的查询逻辑变化非常大,通常不建议分库分表

  数据增长量大、读多写少、逻辑固定适合分库分表。

新的问题与解决方案

  分库分表能够解决单库单表数据量过大、并发压力过高等问题,但数据拆分之后,也会带来一系列新的问题。

  在单库单表的情况下,数据库可以天然完成数据查询、排序、分页、聚合、JOIN、事务以及唯一约束等操作。而分库分表之后,数据被分散到了不同的数据库和数据表中,这些操作有些会变得更加复杂,有些甚至无法直接通过数据库完成。

  例如,查询可能需要同时访问多个分片,分页和排序需要对多个分片的结果进行归并,跨库操作会产生分布式事务问题,而数据库原本提供的唯一约束也可能无法保证全局唯一,另外,如果遇到数据迁移和扩容需求,还得保证处理过程中的数据一致性。

  因此,分库分表不仅要考虑“如何拆”,还要考虑“拆完之后怎么办”。下面就从实际业务场景出发,分别讨论分库分表可能遇到的问题以及对应的解决方案。

  这些可能的讨论,基本都是围绕着“分、写、查、留”四字来讲的:

  • :数据拆开了
    • 怎么路由?
    • 不带分片键怎么办?
    • 数据倾斜怎么办?
  • :事务拆开了,怎么处理
    • 跨库事务
    • 数据一致性
  • :查询拆开了
    • 分页怎么办?
    • 排序怎么办?
    • 聚合怎么办?
    • JOIN 怎么办?
  • :数据规模继续增长,怎么处理:
    • 扩容
    • 数据迁移
    • 冷热分离

分:数据路由问题

  • 分片键选择:具体业务具体分析,一般组合多个字段选择
  • 分片路由:中间件内部处理
  • 查询不带分片键:跨库/表聚合处理
  • 分片空洞:根据业务情况规划好
  • 热点数据倾斜:大多数情况不需要考虑,需要考虑的时候可以针对倾斜数据的服务器分配更多资源

查询不带分片键怎么办?

  这个问题在实际业务里非常常见。

  比如 GPS 表按照employee_id分片,那么下面的 SQL 处理没什么问题:

1
2
3
4
SELECT *
FROM gps_track
WHERE employee_id = 10086
AND gps_time BETWEEN ...;

  因为系统知道10086 → db_2 → table_07只查询一个分片。

  但是,如果业务突然出现:

1
2
3
SELECT *
FROM gps_track
WHERE gps_time BETWEEN '2026-09-06 00:00:00' AND '2026-09-06 23:59:59';

  没有employee_id,那么系统不知道数据在哪。于是只能广播查询 / Scatter-Gather 所有库表:

1
2
3
4
5
db0 → 查询
db1 → 查询
db2 → 查询
db3 → 查询
...

方案一:让核心查询必须携带分片键

  例如业务定义为:查询某个员工轨迹,那么 API 就设计成:GET /employee/{employeeId}/tracks,而不是:GET /tracks?date=2026-09-06

方案二:非核心查询交给 ES

  例如:查询 2026-09-06 所有异常轨迹、查询某区域所有员工轨迹、查询所有迟到员工、按照 GPS 区域搜索员工

  这些查询天然不是按照employee_id进行的。这种情况下,与其让 MySQL:16 个分片全部查询 → 16 个结果集合并 → 排序 → 分页。

  不如让 MySQL 作为交易数据库,负责“正确地存储和修改”,ES 作为查询数据库,负责“灵活地查询”。

写:数据一致性问题

  • 跨库事务:从业务设计上尽量避免跨库事务。
  • 分布式 ID:使用分布式唯一 ID 解决
  • 全局唯一约束:交由分布式中间件生成

查:数据访问问题

  • 分页
  • 排序
  • 聚合
  • JOIN
  • 全局查询

  这些问题简单来说,初期基本可以交给中间件处理,最差的情况也就是多表/多库联查。

分页处理

  这是分库分表非常典型的问题。

  假设:

1
2
3
4
SELECT *
FROM order
ORDER BY create_time DESC
LIMIT 100000, 20;

  单库的时候 MySQL 可以处理,但是现在 db0、db1、db2、db3,每个库都可能存在符合条件的数据。

  要查询:全局第 100001~100020 条订单,则不能简单地:每个库 LIMIT 100000,20,因为最终结果不是这么计算的。

  实际上需要:

1
2
3
4
5
6
7
8
db0 → Top 100020
db1 → Top 100020
db2 → Top 100020
db3 → Top 100020

全局归并

取 100001~100020

  数据量一大,代价很高。

  那么有什么好的方案嘛?

方案一:禁止深分页

  这是生产系统里非常常见的方式,比如最多查询前 10000 条,或者直接采用:基于游标的分页

  例如:

1
2
3
WHERE id < #{lastId}
ORDER BY id DESC
LIMIT 20

  下一页把上一页最后一个 ID 传回来。这比 LIMIT 1000000,20 合理得多。

方案二:使用时间 + ID 做游标

  例如订单:

1
2
3
4
5
6
7
8
WHERE
create_time < #{lastCreateTime}
OR (
create_time = #{lastCreateTime}
AND id < #{lastId}
)
ORDER BY create_time DESC, id DESC
LIMIT 20;

  这样可以避免大量 OFFSET。

排序处理

  例如:查询最近创建的 20 个订单,订单被分散到:db0、db1、db2、db3,每个数据库都查询:

1
2
ORDER BY create_time DESC
LIMIT 20

得到:

1
2
3
4
db0 → 20条
db1 → 20条
db2 → 20条
db3 → 20条

  然后应用程序做:多路归并,类似:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
db0: 100 90 80 ...
db1: 98 95 70 ...
db2: 99 89 60 ...
db3: 97 96 50 ...



100
99
98
97
96
95
...

  最终得到全局 Top 20。

  所以:分库分表以后,ORDER BY 不再天然意味着“全局排序”,而是:分片内部排序 → 应用层归并。

COUNT、SUM、GROUP BY 处理

  例如运营人员想看:今天一共有多少订单?

  单库查看非常简单:

1
2
3
SELECT COUNT(*)
FROM orders
WHERE create_time >= '2026-09-06';

  分库之后:

1
2
3
4
db0 → 100万
db1 → 120万
db2 → 80万
db3 → 110万

  应用层:

1
100 + 120 + 80 + 110 = 410万

  所以简单的:COUNT、SUM 可以:各分片计算 → 汇总。

  但是:GROUP BY 会更复杂。

  例如:

1
2
3
SELECT status, COUNT(*)
FROM orders
GROUP BY status;

  每个分片:

1
2
3
4
5
6
7
db0:
SUCCESS 100
FAIL 10

db1:
SUCCESS 200
FAIL 20

  最后还要:

1
2
SUCCESS = 100 + 200
FAIL = 10 + 20

更复杂的聚合怎么办?

  例如:过去一年每个城市每个月的订单量

  这类统计查询如果全部压在分库后的 MySQL 上,会越来越复杂。

  实际工程通常会逐渐引入:

1
2
3
4
5
6
7
MySQL

CDC / MQ

ES / ClickHouse / Doris / StarRocks

报表分析

  也就是说:不要让在线交易数据库同时承担复杂分析数据库的职责。

JOIN 怎么办?

  这是分库分表最麻烦的问题之一。

  例如:orders、users

  原来:

1
2
3
4
5
SELECT *
FROM orders o
JOIN users u
ON o.user_id = u.id
WHERE o.id = ...;

  现在:

1
2
orders → order_db_0
users → user_db_3

  怎么办?如果两个表不在同一个库:MySQL无法像单库一样自然 JOIN。

方案一:按照相同分片键分片

  这是最理想的方式。

  例如:

1
2
3
user
order
payment

  全部按照:user_id 分片,那么 user_id = 10086 永远:

1
2
3
user_10086 → db2
order_10086 → db2
payment_10086 → db2

  于是相关数据天然在同一个库。

  这叫:绑定表 / 同库分片。

方案二:冗余数据

  例如订单页面经常需要:订单号、用户姓名、用户手机号、商品名称、订单金额。

  那么不要每次:Order → User → Product 跨库查询。

  可以在订单表冗余user_nameuser_phoneproduct_name字段,在订单创建的时候就保存下来,这样查询订单时直接查订单库,方便快捷。

方案三:应用层 JOIN

  例如:订单查询出 user_id → 批量查询用户 → Java 内存中组装

  可以使用:IN (id1,id2,id3…) 批量查询,而不是:N + 1 查询

留:数据运维问题

  初期不用考虑,后期需要再具体讨论来解决这些问题:

  • 扩容
  • 数据迁移
  • DDL
  • 数据校验
  • 监控
  • 冷热数据管理
  • 历史数据归档
  • ES / ClickHouse / 数据仓库

扩容

  实际生产怎么扩容?常见方法有几个。

方案一:预留足够多的分片

  例如一开始就采用 32库 × 32 表这样比较大的设计。

  虽然初期数据量只有 1 亿,但逻辑上提前设计好了,随着业务增长:逻辑分片 → 物理分片,可以逐渐扩容。

方案二:一致性哈希

  此方案可以减少扩容造成的数据迁移。

  但要注意:一致性哈希并不是分库分表的万能扩容方案,数据库分片更常见的还是:预分片、数据迁移、路由规则切换

方案三:数据迁移 + 双写 + 切换

  这是大型系统采用的比较典型的方案:

1
2
3
4
5
旧集群

历史数据迁移

新集群

  同时:旧集群 ←→ 增量数据同步 → 新集群

  等新旧数据一致以后:流量→ 新集群

  最后:停止旧集群

最佳实践

  当业务系统运行一段时间后,发现某张表的数据量越来越大,单库的 CPU、内存、磁盘、IO 等资源逐渐达到瓶颈时,这个时候我们需要对这张表进行分库分表。

  那么,直接将这张表拆了就可以了嘛?

  当然不是,实际落地过程中,我们需要考虑很多维度的事情:

  • 这张表从业务角度考虑,需要选择哪一个或者几个字段作为分片键?
  • 分几个库几个表呢?
  • 公司的服务器资源能给到多少?
  • 期望读写能力提升为当前的多少倍?
  • 如何平滑过渡对旧逻辑减少影响或者不影响?
  • 这张表只有我们在用嘛?有其他同事在用嘛?对他们有没有影响?

  因此,我们评估是否需要拆分时,需要设计一个详细的技术方案,大致按以下步骤进行:

  • 流程梳理及影响评估
  • 方案选型
  • 拆分 SOP (重要)
  • 稳定性保障
  • 技术方案内部评审及优化
  • 同步相关影响方:可能需要一些下游配合改造,需要提前通知他们进行拆分

  除此之外,我们会根据不同的业务场景来选择不同的链路处理,比如 C 端和 B 端链路就不同,旧系统和新系统也不同,因此处理步骤上会有一些差异。

不同业务下的落地链路

不同业务下的落地链路

完整链路:存量 + 增量 + 下游

  适合大型 C 端系统、核心业务系统,既有大量历史数据,又有 Databus、数仓等下游依赖。

1.目标评估

评估:拆成几个库、几个表
目标:读写能力提升X倍、负载降低 Y%、容量要支持未来 Z 年发展。
举例:当前 20 亿,5 年后评估为 100 亿。那么需要分几个表?分几个库?
解答: 一个合理的答案,1024个表,16个库。
按 1024 个表算,拆分完单表 200 万,5 年后为 1000 万。

2.确定分片算法

  根据具体的业务情况选择分片规则,一般选择哈希分片或者日期分片规则即可。

3.选择分表字段

  核心思路:合理选择,尽量减少出现跨库、跨表查询

4.资源准备

  向领导或者运维人员申请好对应的数据库资源。

5、代码改造

  • 配置好对应的分库分表规则
  • 代码改造
    • 写入逻辑过渡:单写老库 → 双写 → 单写新库
    • 读取逻辑过渡:读老库 → 部分读老部分读新库 → 读新库
    • 灰度到全量放开:指定门店灰度、比例灰度 → 全量用户

6.增量数据同步(双写)

  作用:保证增量数据在新库和老库都有

  方案:

  • 同步双写:同步写新库和老库
  • 异步双写:写老库,监听 binlog 异步同步到新库
  • 中间件同步工具:通过一定的规则将数据同步到目标库表

注意点:写新库异常不能影响原有流程

7.全(存)量数据迁移

  作用:迁移老库历史数据,保证新库有全量数据

  方案:

  • RD 开发 Job:查询老库数据,批量循环写入新库
  • 中间件同步工具:通过一定的规则将数据同步到目标库表

  为了降低对数据库的压力和防止数据出错,注意:

  • 控制好同步速率
  • 尽量在业务低峰期执行
  • 若与增量数据交叉了(尽量避免),考虑好并发问题

8.数据一致性校验、补偿

  作用:确保新库数据正确,达到切读标准、检查是否存在改造遗漏点

  方案:增量数据校验、全量数据校验、人工抽检

  核心流程:

  • 读取老库数据
  • 读取新库数据
  • 比较新老库数据:
    • 一致则继续比较下一条数据
    • 不一致则进行补偿:
      • 新库存在,老库不存在:新库删除数据
      • 新库不存在,老库存在:新库插入数据
      • 新库存在、老库存在:比较所有字段,不一致则将新库更新为老库数据

9.灰度切读

  作用:开始将流量切到新库

  原则:

  • 有问题及时切回老库
  • 灰度放量先慢后快,每次放量观察一段时间
  • 支持灵活的规则:
    • 业务切:门店维度灰度
    • 比例切:、百(万)分比灰度
    • 全量切:一次性全部切过去

10. databus 切新库

  作用:将数据库 Binlog 消费链路从老库切换到新库。

  分库分表后,数据库从老库变成 n 个新库db_0、db_1、...、db_n,因此需要让 Databus 同时监听新的分库,并建立逻辑链路:新库 → 新 Databus → 下游。

  迁移期间可以先启动新 Databus,使下游暂时同时接收新老库 Binlog:

1
2
3
老库 → 老 Databus ─┐
├→ 下游
新库 → 新 Databus ─┘

  观察新链路稳定后关闭老 Databus,最终:新库 → 新 Databus → 下游

  重点关注:消息重复、幂等、顺序、延迟、Binlog 位点。

  核心流程:

  • 启动新库 databus,此时下游会同时收到新老库的 binlog
  • 观察一段时间是否正常
    • 有问题及时关闭
    • 没问题后关掉老库 databus

11.下游切数据源

  作用:确保下游系统迁移到新库。

  这里的下游系统一般是数仓,例如数仓原来逻辑为:老库 → ETL → 数仓,迁移后需要改为:新库 → ETL → 数仓。

  注意确认:

  • 新库数据完整;
  • 同步任务能够正确访问所有分库分表;
  • 数据量、字段、数据口径一致;
  • 新旧数据切换期间不存在重复或遗漏。

数仓一般是每天同步一次数据,因此在指定时间内切换即可。

12.停写老库

  原则:确认老库数据源全部迁移后,停写老库。

  至此,分库分表流程处理结束。

  后续逐步将老数据库资源逐渐下线。

B 端系统:业务切换链路

  很多 B 端系统没有复杂的 Databus、数仓链路,或者下游对数据库 Binlog 没有强依赖,因此完整步骤可以简化为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
容量评估

确定分片规则

资源准备

代码改造

增量同步

全量迁移

数据校验

灰度切读

停写老库

老库下线

  核心是:数据迁移 + 业务流量切换,因此所以通常不需要第 10、11 步的Databus 切新库和下游切数据源。

  由于很多系统没有 Databus,也没有复杂的数仓依赖,因此只需要保证业务系统自己从老库切到新库即可。

新系统/无需迁移历史数据:直接分片

  还有一种情况更加简单:旧数据不需要迁移,或者可以舍弃旧数据,直接从新库开始写。

  例如某些日志、轨迹、临时数据系统,历史数据已经过期,或者业务允许重新开始积累。

  那么就不需要:

  • 全量数据迁移
  • 数据校验补偿
  • Databus 切换
  • 下游数据源切换

  新库直接承接新数据,不做历史数据迁移,因此链路简单直接:

1
2
3
4
5
6
7
8
9
10
11
12
13
容量评估

确定分片规则

资源准备

代码改造

直接写新库

直接验证

全量使用

三种链路对比

类型 核心链路 典型场景
完整迁移型 迁增量 → 迁存量 → 校验 → 切读 → 切 Databus → 切下游 → 停老库 大型 C 端、核心系统
业务切换型 迁增量 → 迁存量 → 校验 → 切读 → 停老库 普通 B 端业务系统
新库直切型 代码改造 → 新库直接写 → 直接 → 全量 无需保留历史数据的系统
0%