Skip to content

MySQL 基础见《MySQL 快速入门》,查询优化见《MySQL 索引详解》。这篇解决最后一个大问题:单表数据量太大(几千万上亿行)怎么办——分库分表。这是分布式系统面试必问、也是业务量大了必须面对的。

一、为什么需要分库分表

单表数据量大的两个瓶颈

瓶颈原因表现
查询慢B+Tree 层数增加 + 磁盘 IO 变多即使走索引也慢(百万行 → 千万行,性能断崖)
写入慢单库单表锁竞争、单机磁盘/内存上限高并发写扛不住

经验阈值:单表超过 1000 万行(或 10GB)就该考虑拆分——不是绝对,看业务和查询模式。

注意:分库分表是最后的手段——先确认不是索引/慢 SQL 问题(见《MySQL 索引详解》),能用归档/冷热分离解决的别分。

二、分库分表四象限

分库分表四象限

方式拆什么解决什么适用
垂直分库按业务把表分到不同库库太多/单库压力大多业务系统
垂直分表一张表按列拆(冷热分离)大字段拖慢查询少用(一般用归档)
水平分库同一张表拆到多个库(不同机器)单库写入压力高并发写
水平分表(最常用)同一张表拆成 N 张同结构表单表数据量太大大数据量查询

最常用组合:水平分表(解决数据量)+ 垂直分库(解决多业务)。

三、分表策略(核心:数据怎么路由)

水平分表后,插入/查询时怎么知道去哪张表?三种策略:

java
// ① 哈希取模(最常用):userId % 表数量 → 表号
int table = userId % 4;   // user_0 / user_1 / user_2 / user_3
// 优点:数据均匀分布   缺点:加表要迁移(2 的幂次可以规避部分问题)

// ② 范围分表:按 ID 区间
// user_0: id 1-1000万, user_1: id 1001万-2000万...
// 优点:加表不用迁移(直接加新区间)  缺点:热点集中(新数据都在最后一张表)

// ③ 一致性哈希:解决加节点时迁移量大的问题
// 哈希环 + 虚拟节点,加机器只迁移部分数据
// 优点:扩容友好   缺点:实现复杂、数据分布不均需虚拟节点

取模 vs 范围的取舍:取模数据均匀(写性能好)、范围扩容方便(读性能好)。多数业务用取模,配合2 的幂次(4/8/16 张表)减少扩容迁移。

四、分库分表后的三大问题

分完不是结束——三大新问题必须解决:

1. 跨库 JOIN(最痛)

原来一条 SQL 关联多张表,分库后不同库的表不能 JOIN

sql
-- ❌ 原来:订单表和用户表 JOIN(分库后在不同库,连不了)
SELECT * FROM order JOIN user ON order.user_id = user.id;

-- ✅ 方案:
-- ① 冗余字段:订单表冗余 user_name(查订单不用 join)
-- ② 反查:先查用户得到 ID,再查订单(应用层两次查询)
-- ③ 宽表/数仓:离线同步后查宽表

2. 分布式事务

原来一个库内事务,现在跨库跨服务——需要分布式事务方案(2PC/TCC/本地消息表/事务消息),见《系统架构设计常见问题 FAQ》的"分布式事务"部分。

3. 分布式 ID

自增主键在分表后不唯一(user_0 和 user_1 都有 id=1)——需要全局唯一 ID:

雪花算法(Snowflake)——最常用方案,64 位长整型:

0 | 41位时间戳 | 10位机器ID | 12位序列号
-    毫秒级时间       机器标识      同毫秒内自增
= 每秒可生成 4096*2^10 个,全局唯一、趋势递增
java
// 雪花算法核心逻辑(简化版)
long id = (timestamp << 22) | (machineId << 12) | sequence;

特点:全局唯一 + 趋势递增(对数据库索引友好)+ 无中心化依赖(各机器自己生成)。替代方案:UUID(唯一但无序,做主键会让索引频繁分裂,不推荐做主键)。

五、中间件:ShardingSphere

手写路由逻辑太麻烦(每个 DAO 都要算表号)——ShardingSphere(Apache 开源,国内主流)自动处理:

yaml
# ShardingSphere 配置(逻辑表 user → 实际 4 张表)
rules:
  - !SHARDING
    tables:
      user:
        actualDataNodes: ds_${0..1}.user_${0..1}   # 2 库 × 2 表
        tableStrategy:
          standard:
            shardingColumn: id
            shardingAlgorithmName: user_hash
    shardingAlgorithms:
      user_hash:
        type: HASH_MOD
        props:
          sharding-count: 2        # 取模数量
java
// 业务代码无感知——还是写 user 表,中间件自动路由到 user_1/user_2
userMapper.insert(user);   // ShardingSphere 自动按 id 取模路由

ShardingSphere 做的事:SQL 解析 → 路由到正确分片 → 执行 → 结果合并(自动处理 limit/order by 跨分片)。

六、实跑验证:取模路由 + 分表演示

java
// 模拟分表路由(userId % 4 → 4 张表)
int tableNo = userId % 4;
String table = "user_" + tableNo;
// userId=1 → user_1, userId=5 → user_1(数据均匀分布)
sql
-- 分表后实际建的表(4 张结构相同)
CREATE TABLE user_0 (id INT PRIMARY KEY, name VARCHAR(50), age INT);
CREATE TABLE user_1 (id INT PRIMARY KEY, name VARCHAR(50), age INT);
-- ... user_2, user_3 相同结构

七、什么时候不该分库分表(重要)

情况建议
单表 < 500 万行别分——分库分表引入的复杂度远超收益
只是查询慢先加索引/优化 SQL(见索引详解)
数据可归档先归档冷数据(按年/月拆表)
读写分离可解决用主从复制(读写分离)而不是分表

分库分表是"有代价的复杂度":跨库 JOIN、分布式事务、分布式 ID、扩容迁移——每一样都是坑。数据量到了再分,别提前设计过度。

小结

  • 单表 1000 万行左右考虑拆分(先排索引/归档问题)
  • 四象限:垂直分库(按业务)/ 水平分表(按数据,最常用)
  • 分表路由:哈希取模(均匀)vs 范围(好扩容)
  • 三大问题:跨库 JOIN(冗余/反查)、分布式事务、分布式 ID(雪花算法)
  • 中间件 ShardingSphere:SQL 自动路由合并,业务无感知
  • 别过度设计:数据量到了再分

MySQL 系列到此完整:快速入门 → 事务与隔离级别 → 索引详解 → 分库分表。下一步可以看《系统架构设计常见问题 FAQ》(分布式事务/缓存/高并发)或《Redis 快速入门》(缓存加速)。