ShardingSphere 分库分表实战与原理

🎯 面试题:什么时候需要分库分表?如何选择分片键?

分库分表是解决单表数据量过大、单库并发过高的核心方案。ShardingSphere 是目前最主流的分库分表中间件。


一、什么时候需要分库分表?

单表数据量超过 2000 万行 → 索引 B+ 树层数增加,查询变慢
单库 QPS 超过 5000 → 数据库连接数、CPU 成为瓶颈
单库存储超过 500GB → 备份恢复时间过长

优先考虑的优化手段(按顺序):
  1. 加索引、优化 SQL
  2. 读写分离(主库写,从库读)
  3. 缓存(Redis 缓存热点数据)
  4. 垂直分表(大字段拆分到另一张表)
  5. 垂直分库(按业务模块拆分数据库)
  6. 水平分表(同一张表数据分散到多张表)
  7. 水平分库(数据分散到多个数据库)

二、ShardingSphere 产品线

产品 部署方式 适用场景
ShardingSphere-JDBC 客户端 JAR Java 应用,无需额外部署
ShardingSphere-Proxy 独立代理服务 多语言、DBA 管理
ShardingSphere-Sidecar K8s Sidecar 云原生场景

三、分片策略

哈希分片

# application.yml
spring:
  shardingsphere:
    datasource:
      names: ds0, ds1
      ds0:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://db0:3306/order_db
        username: root
        password: 123456
      ds1:
        type: com.zaxxer.hikari.HikariDataSource
        jdbc-url: jdbc:mysql://db1:3306/order_db
        username: root
        password: 123456

    rules:
      sharding:
        tables:
          t_order:
            actual-data-nodes: ds${0..1}.t_order_${0..3}  # 2库 × 4表 = 8张表
            database-strategy:
              standard:
                sharding-column: user_id
                sharding-algorithm-name: db-hash
            table-strategy:
              standard:
                sharding-column: order_id
                sharding-algorithm-name: table-hash
            key-generate-strategy:
              column: order_id
              key-generator-name: snowflake

        sharding-algorithms:
          db-hash:
            type: HASH_MOD
            props:
              sharding-count: 2
          table-hash:
            type: HASH_MOD
            props:
              sharding-count: 4

        key-generators:
          snowflake:
            type: SNOWFLAKE

范围分片

# 按时间范围分片(适合日志、订单等时序数据)
sharding-algorithms:
  time-range:
    type: RANGE
    props:
      # 2024年数据 → t_order_2024
      # 2025年数据 → t_order_2025
      range-lower: 20240101
      range-upper: 20241231
      sharding-suffix-pattern: yyyy
      datetime-pattern: yyyyMMdd

四、分库分表后的核心问题

1. 分布式 ID

// 雪花算法(Snowflake)
// 64 bit = 1(符号) + 41(时间戳ms) + 10(机器ID) + 12(序列号)
// 每毫秒最多生成 4096 个 ID,理论 QPS = 4096 * 1000 = 400万

// 号段模式(美团 Leaf)
// 从数据库批量获取 ID 段,减少数据库压力
// 每次取 1000 个 ID,用完再取
CREATE TABLE id_alloc (
    biz_tag VARCHAR(128) NOT NULL,
    max_id BIGINT NOT NULL,
    step INT NOT NULL,
    PRIMARY KEY (biz_tag)
);

2. 跨库查询

// ❌ 跨库 JOIN(ShardingSphere 支持但性能差)
SELECT o.*, u.name FROM t_order o JOIN t_user u ON o.user_id = u.id

// ✅ 方案一:冗余字段(反范式)
// 在 t_order 中冗余 user_name 字段,避免跨库 JOIN

// ✅ 方案二:广播表(字典表)
// 小表(如城市、分类)在每个分片都保存一份
spring:
  shardingsphere:
    rules:
      sharding:
        broadcast-tables:
          - t_city
          - t_category

// ✅ 方案三:应用层 JOIN
// 先查 t_order,再根据 user_id 批量查 t_user
List<Order> orders = orderMapper.findByUserId(userId);
List<Long> userIds = orders.stream().map(Order::getUserId).collect(toList());
Map<Long, User> userMap = userMapper.findByIds(userIds).stream()
    .collect(toMap(User::getId, u -> u));

3. 分页查询

// ❌ 深分页问题:SELECT * FROM t_order LIMIT 10000, 10
// ShardingSphere 需要从每个分片取 10010 条,再归并排序,性能极差

// ✅ 方案一:禁止深分页,用 search_after 游标
// ✅ 方案二:二次查询法
// 第一次:每个分片取 LIMIT 10010,取最小 offset 的 ID
// 第二次:WHERE id >= minId LIMIT 10

// ✅ 方案三:ES 存储分页数据,MySQL 存储完整数据

4. 分布式事务

# ShardingSphere 支持 XA 分布式事务
spring:
  shardingsphere:
    rules:
      transaction:
        default-type: XA
        provider-type: Atomikos

# 或者使用 Seata AT 模式(推荐)
spring:
  shardingsphere:
    rules:
      transaction:
        default-type: BASE
        provider-type: Seata

五、面试高频题

Q1: 如何选择分片键?

分片键选择原则:① 均匀分布:数据能均匀分散到各分片,避免热点(如用 user_id 哈希,不用 status);② 查询频率高:大多数查询都带分片键,避免全分片扫描;③ 不可变:分片键一旦确定不能修改(修改需要迁移数据);④ 业务相关:如订单系统用 user_id,日志系统用时间。常见问题:用自增 ID 做分片键会导致数据集中在最新分片(热点)。

Q2: 分库分表后如何处理跨库 JOIN?

三种方案:① 冗余字段(反范式):在表中冗余关联字段,避免 JOIN;② 广播表:小字典表在每个分片都保存一份;③ 应用层 JOIN:先查主表,再批量查关联表,在应用层组装。推荐优先用冗余字段,其次广播表,最后应用层 JOIN。跨库 JOIN 虽然 ShardingSphere 支持,但性能差,不推荐。

Q3: 分库分表后分页查询怎么做?

深分页是分库分表的经典难题。ShardingSphere 默认会从每个分片取 offset+limit 条数据再归并,深分页性能极差。解决方案:① 禁止深分页,改用游标分页(search_after);② 二次查询法:先取各分片最小 offset 的 ID,再用 WHERE id >= minId 精确查询;③ 将分页数据同步到 ES,用 ES 做分页查询,MySQL 只做精确查询。