数据库分库分表设计与实践:从理论到ShardingSphere落地

By | 2026年9月30日

为什么需要分库分表?

当单表数据量超过千万级,数据库的写入和查询性能会急剧下降。索引膨胀、DDL 阻塞、单机容量瓶颈等问题接踵而至。分库分表是解决这些问题的银弹之一,但设计不当反而会引入更多复杂性。

本文将从业务场景出发,带你一步步设计分库分表方案,并使用 Apache ShardingSphere 实现一个可运行的水平拆分示例。

一、分库分表的核心概念

1.1 垂直拆分 vs 水平拆分

  • 垂直分库:按业务拆分,如用户库、订单库、商品库。降低单库压力,但无法解决单表数据量过大问题。
  • 垂直分表:将不常用字段或大字段拆分到扩展表,如 user 和 user_profile。
  • 水平分库:同一张表的数据按规则分散到多个库。
  • 水平分表:同一张表的数据按规则分散到多个表。

通常组合使用:先垂直拆分,再水平拆分。

1.2 分片键(Sharding Key)的选择

分片键决定了数据分布和查询路由。最佳实践:

  • 选择高频查询条件作为分片键,如 userid、orderid。
  • 避免使用时间戳作为唯一分片键,容易导致热点(新数据集中在一个分片)。
  • 尽量保证数据均匀分布,如使用 userid % 4 或 hash(userid) % 4。

💡 注意:如果查询条件不带分片键,将触发全路由(扫描所有分片),性能极差。

二、分片算法与路由

2.1 常见分片算法

  • 取模:user_id % 4,简单但扩容困难。
  • 范围:按 ID 区间或时间范围,易扩容但可能热点。
  • 一致性哈希:扩容时影响小,但实现复杂。
  • 雪花算法 + 取模:保证 ID 全局唯一且均匀。

2.2 路由类型

  • 直接路由:分片键能直接定位。
  • 标准路由:最常见,= 和 IN 查询。
  • 笛卡尔路由:多分片键关联查询,性能差,尽量避免。

三、实战:Spring Boot + ShardingSphere 水平分表

我们模拟订单表 torder,按 userid 分 2 个库,每个库分 2 张表,共 4 个分片。

3.1 环境准备

  • JDK 1.8+
  • MySQL 5.7+(两个库:db0、db1)
  • Spring Boot 2.7.x
  • ShardingSphere-JDBC 5.3.0

建表 SQL(在每个库中执行):


CREATE TABLE t_order_0 (
  order_id BIGINT NOT NULL,
  user_id BIGINT NOT NULL,
  order_no VARCHAR(64),
  amount DECIMAL(10,2),
  create_time DATETIME,
  PRIMARY KEY (order_id)
);

CREATE TABLE t_order_1 (
  order_id BIGINT NOT NULL,
  user_id BIGINT NOT NULL,
  order_no VARCHAR(64),
  amount DECIMAL(10,2),
  create_time DATETIME,
  PRIMARY KEY (order_id)
);

3.2 添加依赖


<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
    <version>5.3.0</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.33</version>
</dependency>

3.3 配置分片规则

application.yml:


spring:
  shardingsphere:
    datasource:
      names: db0, db1
      db0:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://localhost:3306/db0?useSSL=false
        username: root
        password: 123456
      db1:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://localhost:3306/db1?useSSL=false
        username: root
        password: 123456
    rules:
      sharding:
        tables:
          t_order:
            actual-data-nodes: db${0..1}.t_order_${0..1}
            database-strategy:
              standard:
                sharding-column: user_id
                sharding-algorithm-name: db-inline
            table-strategy:
              standard:
                sharding-column: user_id
                sharding-algorithm-name: table-inline
        sharding-algorithms:
          db-inline:
            type: INLINE
            props:
              algorithm-expression: db${user_id % 2}
          table-inline:
            type: INLINE
            props:
              algorithm-expression: t_order_${user_id % 2}
    props:
      sql-show: true

3.4 编写实体与 Mapper


@Data
@TableName("t_order")
public class Order {
    private Long orderId;
    private Long userId;
    private String orderNo;
    private BigDecimal amount;
    private LocalDateTime createTime;
}

@Mapper
public interface OrderMapper extends BaseMapper<Order> {
}

3.5 测试插入与查询


@SpringBootTest
class OrderTest {
    @Autowired
    private OrderMapper orderMapper;

    @Test
    void testInsert() {
        for (long userId = 1; userId <= 4; userId++) {
            Order order = new Order();
            order.setOrderId(userId * 1000);
            order.setUserId(userId);
            order.setOrderNo("NO" + userId);
            order.setAmount(new BigDecimal("99.9"));
            order.setCreateTime(LocalDateTime.now());
            orderMapper.insert(order);
        }
    }

    @Test
    void testSelect() {
        // 带分片键,路由到具体分片
        Order order = orderMapper.selectById(1000L);
        System.out.println(order);

        // 不带分片键,全路由(慎用)
        List<Order> list = orderMapper.selectList(null);
        System.out.println(list.size());
    }
}

执行后,观察日志可以看到 SQL 被路由到 db0.torder0 等具体分片。

💡 注意:selectById 使用 orderid 作为主键,但分片键是 userid,因此会全路由。生产环境应避免这种查询,或建立 orderid 与 userid 的映射关系。

四、分布式主键生成

分库分表后,自增主键不再全局唯一。推荐使用雪花算法(Snowflake)。ShardingSphere 内置了 SNOWFLAKE 算法:


spring:
  shardingsphere:
    rules:
      sharding:
        tables:
          t_order:
            key-generator:
              column: order_id
              type: SNOWFLAKE

这样插入时无需手动设置 order_id。

五、分布式事务与数据一致性

跨库操作需要分布式事务。ShardingSphere 支持:

  • LOCAL:本地事务,适用于单库。
  • XA:强一致,性能较差。
  • BASE:最终一致,基于 Seata。

推荐使用 Seata AT 模式,对业务无侵入。配置示例:


spring:
  shardingsphere:
    rules:
      transaction:
        default-type: BASE
        provider-type: Seata

六、常见坑与最佳实践

6.1 避免跨分片 JOIN

分片后 JOIN 只能在内存中归并,性能极差。解决方案:

  • 冗余字段(如订单表冗余用户名)。
  • 宽表(ETL 同步到 ES)。
  • 绑定表(相同分片规则的表,如 torder 和 torderitem 都按 orderid 分片)。

6.2 分页查询性能

LIMIT 10000, 10 会从每个分片取 10010 条再归并,效率低。优化:

  • 使用二次查询法:先查主键,再回表。
  • 限制最大页数。

6.3 扩容方案

  • 停机扩容:简单但影响业务。
  • 双写迁移:新老库双写,逐步切流。
  • 一致性哈希:减少数据迁移量。

6.4 全局 ID 与分片键冲突

若使用 orderid 作为分片键,但查询常用 userid,可建立基因法:将 userid 的后几位嵌入 orderid,使两者都能路由到同一分片。

七、总结

分库分表是应对海量数据的有效手段,但绝非银弹。设计时需重点考虑:

  1. 分片键选择:高频、均匀、避免热点。
  2. 路由策略:尽量带分片键查询。
  3. 全局 ID:雪花算法或号段模式。
  4. 分布式事务:根据一致性要求选择方案。
  5. 扩容与运维:提前规划,避免后期迁移痛苦。

下一步可以学习 ShardingSphere 的读写分离、数据加密、影子库等功能,构建更完善的分布式数据库架构。

—

延伸阅读:

  • Apache ShardingSphere 官方文档
  • 《数据分片与路由》—— 分布式数据库核心原理
  • Seata 官方文档:AT 模式详解