分库分表实战

系统讲解分库分表策略:水平/垂直拆分原理、分片键设计、Sharding-JDBC 中间件实战、全局 ID 生成方案(雪花算法/号段模式)、平滑扩容与数据迁移方案。

当单库数据量超过千万级、QPS 超过数千时,单节点数据库会成为系统瓶颈。分库分表(Sharding)是解决单机数据库性能与容量限制的核心方案。本文系统讲解拆分策略、分片键设计、中间件选型和扩容迁移方案。


1. 为什么要分库分表

1.1 单库性能天花板

指标MySQL 单节点上限实际问题
连接数~2000连接数耗尽
写入 QPS~3000/s写入瓶颈
数据量~5000万行查询变慢
存储容量~2TB磁盘上限
索引大小内存无法缓存频繁 IO

1.2 分库 vs 分表

┌───────────────────────────────────────────────────────────────┐
│                   垂直拆分(Vertical Sharding)                │
│                                                             │
│   ┌──────────────┐          ┌──────────────┐               │
│   │  user_db     │          │  order_db    │               │
│   │  ├── users   │          │  ├── orders  │               │
│   │  ├── profiles│          │  ├── items   │               │
│   │  └── settings│          │  └── payments│               │
│   └──────────────┘          └──────────────┘               │
│                                                             │
│   特点:按业务拆分,不同表放不同库                            │
│   优点:降低单库压力,业务解耦                               │
│   缺点:无法解决单表大数据量问题,跨库 JOIN 困难              │
└───────────────────────────────────────────────────────────────┘

┌───────────────────────────────────────────────────────────────┐
│                   水平拆分(Horizontal Sharding)              │
│                                                             │
│   ┌──────────────┐          ┌──────────────┐               │
│   │  order_db_0  │          │  order_db_1  │               │
│   │  ├── orders  │          │  ├── orders  │               │
│   │  └── items   │          │  └── items   │               │
│   │ user_id%2=0  │          │  user_id%2=1 │               │
│   └──────────────┘          └──────────────┘               │
│                                                             │
│   特点:同一表按规则拆分到多个库                             │
│   优点:解决单表容量和性能问题                               │
│   缺点:扩容复杂、分布式事务、全局查询困难                     │
└───────────────────────────────────────────────────────────────┘

2. 分片策略

2.1 常见分片算法

算法公式优点缺点适用场景
取模(Modulo)shard = key % N均匀分布、简单扩容需迁移全部数据固定分片数或哈希桶
范围(Range)shard = key / 1000000扩容简单(追加库)、按时间归档热点严重(最新库压力大)按时间/ID 范围
哈希范围一致性哈希 / 虚拟桶扩容平滑、负载均衡实现复杂动态扩容场景
标签(Tag)按业务标签路由灵活、可控需维护映射表多租户/地域分片
复合分片先范围后取模兼顾两者优点逻辑复杂超大规模系统

2.2 取模分片示例

// 用户ID取模分2库4表
public class ShardingStrategy {
    public static ShardInfo route(long userId) {
        int dbIndex = (int) (userId % 2);        // 0 或 1
        int tableIndex = (int) ((userId / 2) % 4); // 0,1,2,3
        return new ShardInfo(dbIndex, tableIndex);
    }
}

// user_id = 10001 → db_1, orders_1
// user_id = 10002 → db_0, orders_2

2.3 范围分片示例

用户ID范围:          库索引:
0     ~ 5,000,000   → db_0
5,000,001 ~ 10,000,000 → db_1
10,000,001 ~ 15,000,000 → db_2
        ...

优点:扩容只需增加新库,老数据无需迁移
缺点:新注册用户都落在新库,热点倾斜

2.4 混合策略:Range + Modulo

第一层:按时间/范围分库(解决扩容)
第二层:按取模分表(解决单库热点)

示例(订单表):
- 2024年 → db_2024
  - 订单表分为 16 张:orders_00 ~ orders_15(按 order_id % 16)
- 2025年 → db_2025(新库)
  - 同样有 orders_00 ~ orders_15

3. 分片键(Sharding Key)设计

3.1 分片键选取原则

原则说明错误示例正确示例
查询频繁最常用 WHERE 条件create_time(无条件查询)user_id
数据均匀避免热点status(inactive 极少)user_id 哈希
避免跨片核心查询单分片解决user_idorders 却按 order_id 分片订单按 user_id 分片
不可变分片键修改代价大email(可能变更)user_id

3.2 典型场景的分片键

业务场景分片键原因
电商订单user_idbuyer_id用户查询自己的订单是最高频操作
社交消息sender_idreceiver_id按会话分片,双向冗余或聚合
日志/O2Ocreated_time按时间归档、清理
多租户 SaaStenant_id数据隔离 + 按租户路由

4. Sharding-JDBC 实战

4.1 引入依赖

<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>shardingsphere-jdbc-core</artifactId>
    <version>5.5.0</version>
</dependency>

4.2 YAML 配置

# application-sharding.yml
dataSources:
  ds_0:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    jdbcUrl: jdbc:mysql://db0:3306/order_db
    username: root
    password: xxx
  ds_1:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    jdbcUrl: jdbc:mysql://db1:3306/order_db
    username: root
    password: xxx

rules:
  - !SHARDING
    tables:
      t_order:
        actualDataNodes: ds_${0..1}.t_order_${0..15}
        tableStrategy:
          standard:
            shardingColumn: user_id
            shardingAlgorithmName: t_order_inline
        databaseStrategy:
          standard:
            shardingColumn: user_id
            shardingAlgorithmName: db_inline
        keyGenerateStrategy:
          column: order_id
          keyGeneratorName: snowflake
    
    shardingAlgorithms:
      db_inline:
        type: INLINE
        props:
          algorithm-expression: ds_${user_id % 2}
      t_order_inline:
        type: INLINE
        props:
          algorithm-expression: t_order_${(user_id % 32).intdiv(2)}
    
    keyGenerators:
      snowflake:
        type: SNOWFLAKE
        props:
          worker-id: 0

4.3 Java 代码示例

@Configuration
public class ShardingConfig {
    
    @Bean
    public DataSource dataSource() throws SQLException {
        return YamlShardingSphereDataSourceFactory.createDataSource(
            getClass().getResource("/application-sharding.yml")
        );
    }
}

@Service
public class OrderService {
    @Autowired
    private OrderRepository orderRepo;
    
    // 分片键自动路由 - 无需关注底层分片
    public Order getOrder(Long userId, Long orderId) {
        return orderRepo.findByUserIdAndOrderId(userId, orderId);
    }
    
    public List<Order> getUserOrders(Long userId) {
        // WHERE user_id = ? 自动路由到单个分片
        return orderRepo.findByUserId(userId);
    }
}

5. 全局 ID 生成

5.1 方案对比

方案原理优点缺点适用
自增 ID + 步长库A: 1,3,5… 库B: 2,4,6…简单扩容困难、ID 不连续小规模
号段模式批量获取 ID 区间高性能、趋势递增依赖数据库大多数场景
雪花算法时间戳+机器ID+序列号去中心化、高性能时钟回拨问题高并发
Leaf 号段+Snowflake美团方案兼顾两者依赖 ZooKeeper超大规模

5.2 雪花算法(Snowflake)

┌─────────────────────────────────────────────────────────┐
│  Snowflake ID (64 bits)                                  │
├──────┬──────────────┬────────────┬──────────────────────┤
│ Sign │ Timestamp    │ Worker ID  │ Sequence             │
│ 1 bit│ 41 bits      │ 10 bits    │ 12 bits              │
├──────┼──────────────┼────────────┼──────────────────────┤
│  0   │ 毫秒时间戳    │ 机器标识    │ 同毫秒内的序列号      │
└──────┴──────────────┴────────────┴──────────────────────┘

Timestamp: 相对于自定义 Epoch 的毫秒偏移(可用约69年)
Worker ID: 支持 1024 个节点
Sequence: 每毫秒可生成 4096 个 ID
public class SnowflakeIdWorker {
    private final long workerId;
    private final long epoch = 1704067200000L; // 2024-01-01
    private long sequence = 0;
    private long lastTimestamp = -1;
    
    public synchronized long nextId() {
        long timestamp = System.currentTimeMillis();
        
        if (timestamp == lastTimestamp) {
            sequence = (sequence + 1) & 0xFFF;
            if (sequence == 0) timestamp = tilNextMillis();
        } else {
            sequence = 0;
        }
        
        lastTimestamp = timestamp;
        return ((timestamp - epoch) << 22) | (workerId << 12) | sequence;
    }
}

5.3 Leaf 号段模式

-- 号段表
CREATE TABLE leaf_alloc (
    biz_tag VARCHAR(128) PRIMARY KEY,
    max_id BIGINT NOT NULL DEFAULT 0,
    step INT NOT NULL,
    update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 获取号段(UPDATE + SELECT for Update 保证原子性)
BEGIN;
UPDATE leaf_alloc SET max_id = max_id + step WHERE biz_tag = 'order';
SELECT max_id, step FROM leaf_alloc WHERE biz_tag = 'order';
COMMIT;
-- 应用获得 [max_id-step+1, max_id] 区间,在内存中分配

6. 扩容与数据迁移

6.1 一致性哈希平滑扩容

初始:4 个节点(虚拟节点数 v=150)
扩容为 6 个节点:

数据迁移量 ≈ 1/4 - 1/6 = 1/12 ≈ 8.3%

优势:只迁移相邻节点的数据,而非全量

6.2 双写迁移方案(不停机)

阶段1:双写
  写请求 → 老库 + 新库(异步)
  读请求 → 老库

阶段2:历史数据迁移
  使用工具(如 DataX/Canal)将老库历史数据灌入新库
  校验数据一致性

阶段3:切读
  灰度读请求 → 新库
  验证正确性后全量切读

阶段4:停双写
  写请求 → 仅新库

6.3 数据校验

# pt-table-checksum 校验主从/分片一致性
pt-table-checksum \
  --host=db0 --user=root --password=xxx \
  --databases=order_db --tables=t_order \
  --replicate=percona.checksums

# pt-table-sync 修复差异
pt-table-sync --execute h=db0,u=root,p=xxx h=db1,u=root,p=xxx \
  --databases=order_db --tables=t_order

7. 分库分表后的新问题

原功能分片后挑战解决方案
全局唯一 ID自增 ID 失效雪花算法/号段模式
跨库 JOIN不支持字段冗余、宽表、ES 聚合
跨库事务分布式事务复杂TCC/Saga/消息补偿/Seata
全局排序/分页需聚合多库结果禁止深分页、ES/ClickHouse
聚合查询(COUNT/SUM)需遍历所有分片定期预计算、流式计算
外键约束失效应用层保证

8. 总结

分库分表是数据库架构演进的重要里程碑:

容量/性能瓶颈
      │
      ▼
┌─────────────────┐
│ 1. 先优化单库    │  ← 索引、SQL、缓存、主从读写分离
│    (成本最低)  │
└────────┬────────┘
         │ 仍然不够
         ▼
┌─────────────────┐
│ 2. 垂直拆分      │  ← 按业务分库
│    (解耦+降负载)│
└────────┬────────┘
         │ 单表仍然过大
         ▼
┌─────────────────┐
│ 3. 水平拆分      │  ← Sharding-JDBC / 自研中间件
│    (终极方案)  │
└─────────────────┘

核心设计要点:分片键 > 分片算法 > 中间件选型 > 全局 ID > 扩容方案。分库分表不是银弹,会带来分布式事务、全局查询等新的挑战,应在充分榨取单库性能后再引入。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「数据库」更多文章

  1. 备份恢复与高可用方案
  2. 数据库性能监控与诊断
  3. NewSQL 选型对比