如何进行分库分表

如何进行分库分表?

核心认知:分库分表不是 MySQL 的调参问题,而是“数据模型 + 路由规则 + 业务改造”的系统工程。
这篇从“要不要拆 → 怎么拆 → 怎么查 → 有啥坑”四个维度来讲。


一、为什么要分库分表?

先说直白一点:不到一定规模,不要轻易分库分表

一般什么时候才需要考虑:

  1. 单表数据量太大

    • 单表上千万甚至上亿行;
    • 索引越来越“高”和“胖”,查询逐渐变慢;
    • DDL(加字段、改索引)一动就是几小时。
  2. 单实例扛不住

    • QPS / TPS 明显逼近单机极限;
    • CPU、IO 经常打满;
    • 即使 SQL & 索引已经优化得差不多。
  3. 冷热数据明显

    • 老数据很少访问,却占着大量存储和索引空间;
    • 影响缓存命中率和 buffer pool 利用率。

如果只是偶尔慢、单表几百万行内、SQL 比较随便,那优先做:

  • 索引设计;
  • SQL 改写;
  • 参数调优;
  • 加从库做读写分离。

分库分表是真正的“大手术”,要慎用。


二、分库 vs 分表:先把概念整明白

  • 分表(Sharding Table):把一个逻辑表拆成多个物理表。
    • 例:order_0order_1order_2
  • 分库(Sharding Database):把数据分布到多个数据库实例上。
    • 例:order_db_0order_db_1

通常配套一起用:

逻辑上:order 表
物理上:order_db_0.order_0 ~ order_3
       order_db_1.order_0 ~ order_3

更细一点:

  • 水平切分(Horizontal Sharding):按“行”切,以某个键做分片;
  • 垂直切分(Vertical Sharding):按“列 / 业务模块”切,把不同业务放到不同库。

2.1 垂直拆分(按业务拆库)

思路:
按业务模块把表拆到不同的库里:

  • user 库:用户、登录、账户;
  • order 库:订单、支付记录;
  • product 库:商品、库存。

适用场景:

  • 业务边界清晰;
  • 模块间关联较少。

优点:

  • 架构清晰,各库职责分明;
  • 避免“一个实例所有表堆一起”。

缺点:

  • 跨库 JOIN 不方便(需要应用层处理);
  • 解决不了“单表数据量巨大”的问题。

2.2 水平拆分(按数据行拆库/拆表)

思路:
同一张表的数据分散到多个库/表中:

  • 比如按照用户 ID 取模、按时间分表等。

适用场景:

  • 单表数据量大;
  • 单实例性能瓶颈。

优点:

  • 每个分片数据量变小,查询更快;
  • 压力分散到多个库实例。

缺点:

  • 路由逻辑复杂;
  • 分布式事务、跨分片聚合、ID 唯一性等问题需要处理。

三、如何设计分片键和分片规则?

这是分库分表最核心的设计点。

3.1 分片键(Sharding Key)怎么选?

原则:

  1. 高频查询条件上必须包含分片键

    • 大部分查询应该能通过分片键直接定位到“一个分片”;
    • 避免“跨所有分片扫一圈”。
  2. 分片键要有一定的“离散性”

    • 避免所有数据堆在一个分片;
    • 避免极端倾斜。
  3. 尽量选“天然存在”的业务字段

    • 用户相关业务:可考虑 user_id;
    • 商户相关业务:可考虑 merchant_id;
    • 订单系统:经常按 user_id / merchant_id 来查时,优先用这些。

不太适合作为分片键的:

  • status 这类取值很少的字段;
  • is_deleted 这种布尔字段;
  • 经常变化的字段。

3.2 分片规则:范围 vs 取模 vs 一致性哈希

常见几种:

1)范围分片(Range Sharding)

按某个范围切:

  • 比如按时间:2024 年的订单在一个库/表,2025 年在另一个;
  • 或按 ID 范围:1~1千万 一片,1千万~2千万 一片。

优点:

  • 便于做“冷热数据分离”(老数据单独放、甚至归档);
  • 一些范围查询在同一分片里。

缺点:

  • 数据倾斜风险大(热门时间段),容易出现热点分片
  • 扩容时需要做“数据搬家”。
2)哈希取模(Hash / Mod Sharding)

按分片键取模:

db_index  = uid % db_count
table_idx = uid % table_count_per_db

优点:

  • 数据分布相对均匀;
  • 容易实现简单路由。

缺点:

  • 扩容时很麻烦:分片数变了,取模结果全乱,要大迁移;
  • 范围查询(尤其跨分片的)很麻烦。

可以通过预留分片(预先设较多分片数)来减轻扩容痛苦。

3)一致性哈希 / 虚拟槽

适用一些分布式中间件:

  • 用一致性哈希将 key 映射到“虚拟槽”;
  • 再将槽映射到物理库表;
  • 扩容/缩容时只需调整局部映射,不用全量迁移。

实现较复杂,一般使用现成中间件(如 ShardingSphere、某些云厂商分布式数据库)。


四、分库分表之后,怎么路由 SQL?

4.1 应用层手写路由

最朴素的方法:

  1. 调用前,根据分片键计算目标库 / 表;
  2. 通过不同的数据源连接对应库表执行。

伪代码示例:

long uid = getUserId();
int dbIndex = (int)(uid % DB_COUNT);
int tableIndex = (int)(uid % TABLE_COUNT_PER_DB);

String dbName = "order_db_" + dbIndex;
String tableName = "t_order_" + tableIndex;

String sql = "SELECT * FROM " + tableName + " WHERE user_id = ? AND order_id = ?";

优点:

  • 可控性高,逻辑完全掌握在自己手里;
  • 没有单点中间件。

缺点:

  • 业务侵入大,维护复杂;
  • 跨表、跨库的 JOIN/统计需要自己写一套框架。

4.2 使用中间件/框架

常见方案:

  • ShardingSphere(Sharding-JDBC / Proxy);
  • MyCAT 等代理型中间件;
  • 各类云厂商的分布式数据库产品。

好处:

  • 提供:路由、读写分离、分布式事务支持、跨分片聚合等;
  • 应用层可以像操作单库一样写 SQL(有局限,但体验好)。

适合:

  • 对自研分片框架成本无暇顾及;
  • 业务复杂、开发团队想省心一点。

五、分库分表后的几个大坑

5.1 跨分片 JOIN 问题

原来可以直接写:

SELECT *
FROM order o
JOIN user u ON o.user_id = u.id
WHERE u.id = ?;

分库分表后:

  • user 表在 user_db,order 表在 order_db;
  • 甚至 order 还按 user_id 水平拆到了多个表;
  • 跨库 JOIN 基本直接废掉

常见解决方案:

  1. 冗余字段

    • 在 order 里冗余 user 的部分字段(如 user_name);
    • 用于展示时不再 JOIN user 表。
  2. 应用层分步查询

    • 先查 user;
    • 根据 user_id 再去对应分片查 order;
    • 手工在内存中做结果组合。
  3. 通过中间件支持分片 JOIN

    • 中间件在后台拆分 SQL、多路查询、做 merge;
    • 适合数据量较小的 JOIN 场景。

5.2 分布式事务问题

跨库操作时:

库 A:减库存
库 B:创建订单

传统单库事务无法覆盖,需要考虑:

  • 2PC(两阶段提交)——复杂且性能一般;
  • TCC、可靠消息、最终一致性等分布式事务方案。

常见做法:

  • 能在同一个分片解决的事务,就不要跨分片
    • 分片键设计时尽量让“事务内的多个表”落在同一分片;
  • 对少数必须跨分片的场景:
    • 使用业务层“补偿机制”的最终一致方案。

5.3 全局唯一 ID 问题

分库分表后:

  • 自增主键(AUTO_INCREMENT)只能保证“单库单表内自增”;
  • 无法保证“全局唯一 + 有序”。

常见解决方案:

  1. 雪花算法(Snowflake)

    • 格式一般是:时间戳 + 机器 ID + 序列;
    • 有序、全局唯一;
    • 不能保证连续。
  2. 号段发号器(Segment)

    • 单独建一张“号段表”;
    • 应用一次取一段 ID(如 1000 个),在内存中发号;
    • 高可用 + 批量减少数据库压力。
  3. 数据库序列表

    • 维护一个 sequence 表,每次 UPDATE + LAST_INSERT_ID;
    • 并发高时压力集中,容易成为瓶颈。

5.4 跨分片统计和分页

比如要做:

统计所有订单总量、总金额
查最近 10 条订单(全局排序)

分片后,数据被打散:

  • 单次查询难以直接从某一个库/表拿到全部结果;
  • 跨分片全量统计 = 多分片聚合;
  • “全局分页” = 多分片取 TopN 再合并排序。

常见方案:

  1. 使用中间件提供的分布式聚合功能(ShardingSphere / MyCAT);
  2. 分场景处理:
    • 常用统计维度提前做 汇总表离线数仓
    • 复杂统计走大数据/OLAP 系统(如 ClickHouse、Hive、Spark);
    • 线上 MySQL 分片只负责“在线交易数据”。

六、如何平滑从单库迁移到分库分表?

这一步很关键,不能“停服一天导数据”。常见迁移路径:

6.1 双写 + 验证

  1. 第一阶段:只写老库,读老库

    • 开发分库分表逻辑;
    • 写一套“同步任务”从老库同步到新分片结构(可用 Canal / binlog 方案);
  2. 第二阶段:新旧双写

    • 应用写请求同时写老库 & 新分片库;
    • 读请求仍然走老库;
    • 对比新旧数据是否一致。
  3. 第三阶段:读新库,写新库

    • 逐步把读请求切到新库;
    • 观察一段时间;
    • 确认稳定后逐步退役老库。

6.2 短暂停写 + 全量迁移(小规模场景)

  • 对于体量较小、业务可短暂停机的系统:

    1. 停机 → 阻塞写入;
    2. 全量导数据到新分片;
    3. 切换应用连接;
    4. 恢复服务。
  • 一般只适合内部系统或低峰期窗口。


七、一个简单的分库分表示例(按 user_id 取模)

假设:

  • 2 个库:order_db_0order_db_1
  • 每个库 4 张表:t_order_0 ~ t_order_3

7.1 分片规则

dbIndex    = user_id % 2
tableIndex = user_id % 4

database: order_db_{dbIndex}
table:    t_order_{tableIndex}

7.2 建表示例

-- order_db_0.t_order_0
CREATE TABLE t_order_0 (
    id         BIGINT PRIMARY KEY,        -- 全局唯一 ID(雪花算法)
    user_id    BIGINT NOT NULL,
    amount     DECIMAL(10,2),
    status     TINYINT,
    create_time DATETIME,
    KEY idx_user_ctime(user_id, create_time)
) ENGINE = InnoDB;

-- 其它 7 张表结构一致,只是表名不同

7.3 应用层路由伪代码

long userId = getUserId();

int dbIndex = (int)(userId % 2);
int tblIndex = (int)(userId % 4);

String dbName = "order_db_" + dbIndex;
String tableName = "t_order_" + tblIndex;

String sql = "SELECT * FROM " + tableName + " WHERE user_id = ? AND id = ?";

八、分库分表的 Checklist(可以贴墙上)

  1. 确认真的需要吗?

    • 单表数据量 > 2000 万?
    • 单实例已经调优到极限?
    • QPS / TPS 常年高位?
  2. 先做垂直拆分,再做水平拆分

    • 按业务拆库 → 单表变少;
    • 再对热点大表按行拆分。
  3. 分片键选好了没?

    • 覆盖主要查询场景?
    • 分布是否均匀?
    • 尽量保证“一个事务内的表”在同一分片?
  4. 分片规则明确了吗?

    • 范围 / 取模 / 一致性哈希?
    • 扩容时如何调整?
  5. 跨分片问题方案:

    • JOIN → 冗余字段 + 应用层拼装;
    • 事务 → 尽量局部分片 + 补偿机制;
    • 统计 → 汇总表 / 数仓 / 中间件。
  6. 全局 ID 方案定了吗?

    • 雪花算法 / 号段发号器 / 其他方案?
  7. 迁移方案准备好了吗?

    • 双写 + 校验?
    • 数据回滚预案?
    • 监控和压测?

九、小结

分库分表的关键不是“语法怎么写”,而是:

  1. 到底要不要拆?
  2. 怎么根据业务特征选分片键和规则?
  3. 拆了之后,路由、ID、事务、JOIN、统计怎么搞?
  4. 如何安全地从单库切到分片架构?

一句话:

把分库分表当成“架构升级”,而不是简单的 MySQL 优化。
想清楚业务访问模式、流量结构和增长预期,然后再动刀,会少掉非常多的坑。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值