一、核心概念

1. 什么是分表(Table Sharding)?

分表是将一个大表的数据按某种规则拆分到多个结构相同但名称不同的物理表中,以提升数据库读写性能和管理效率。

🎯 目的:解决单表数据量过大(如千万级以上)导致的查询慢、锁竞争、备份困难等问题。

2. 常见分表策略

策略 示例 适用场景
按时间分表 order_202501, order_202502 日志、订单、流水等时间序列数据
按ID取模分表 user_0, user_1 ... user_9 用户表,ID 分布均匀
按租户/组织分表 data_tenant1001, data_tenant1002 多租户系统
范围分表 user_0_10w, user_10w_20w ID 连续增长场景

3. MyBatis-Plus 与分表的关系

⚠️ 重要提示
MyBatis-Plus 本身不提供原生分表功能!
它只是一个 ORM 框架,必须结合第三方中间件才能实现自动分表。

✅ 推荐组合:

  • ShardingSphere-JDBC(推荐):轻量级 Java 库,嵌入应用层
  • MyCat:数据库中间件,独立部署
  • Vitess:Kubernetes 生态下的分库分表方案

🔧 本文以 ShardingSphere-JDBC + MyBatis-Plus 为例,讲解完整实践。


二、操作步骤(超详细,适合快速上手)

步骤 1:添加依赖(Maven)

<!-- Spring Boot -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- MyBatis-Plus -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.6</version>
</dependency>

<!-- ShardingSphere-JDBC 核心依赖 -->
<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
    <version>5.4.1</version> <!-- 推荐使用最新稳定版 -->
</dependency>

<!-- 数据库驱动 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

步骤 2:创建分表示例表(MySQL)

假设我们要对 order 表按月分表:

-- 创建 2025 年前两个月的订单表
CREATE TABLE `order_202501` (
  `id` BIGINT NOT NULL AUTO_INCREMENT,
  `user_id` BIGINT NOT NULL,
  `amount` DECIMAL(10,2) NOT NULL,
  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `order_202502` (
  `id` BIGINT NOT NULL AUTO_INCREMENT,
  `user_id` BIGINT NOT NULL,
  `amount` DECIMAL(10,2) NOT NULL,
  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

✅ 所有分表结构必须一致!


步骤 3:创建实体类(MyBatis-Plus 实体)

@Data
@TableName("order") // 逻辑表名,不是真实表名
public class Order {
    private Long id;
    private Long userId;
    private BigDecimal amount;
    
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
}

⚠️ 注意:@TableName("order")逻辑表名,ShardingSphere 会根据规则自动路由到 order_202501 等真实表。


步骤 4:配置 ShardingSphere 分片规则(application.yml)

spring:
  shardingsphere:
    datasource:
      names: ds0
      ds0:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
        username: root
        password: root

    rules:
      sharding:
        # 分片表配置
        tables:
          order:  # 逻辑表名
            actual-data-nodes: ds0.order_2025${1..12}  # 实际数据节点:ds0.order_202501 ~ order_202512
            table-strategy:
              standard:
                sharding-column: create_time
                sharding-algorithm-name: order-by-month

        # 分片算法配置
        sharding-algorithms:
          order-by-month:
            type: CLASS_BASED
            props:
              strategy: STANDARD
              algorithmClassName: com.example.sharding.MonthlyShardingAlgorithm

    # SQL 日志(调试用)
    props:
      sql-show: true

步骤 5:编写自定义分片算法(按时间分月)

@Component
public class MonthlyShardingAlgorithm implements StandardShardingAlgorithm<Comparable<?>> {

    private Properties props;

    // 提取真实表名:根据 create_time 决定路由到哪张表
    @Override
    public String doSharding(Collection<String> availableTargetNames, 
                             PreciseShardingValue<Comparable<?>> shardingValue) {
        LocalDateTime createTime = (LocalDateTime) shardingValue.getValue();
        int year = createTime.getYear();
        int month = createTime.getMonthValue();
        
        // 格式化为 order_202501
        String tableName = String.format("order_%d%02d", year, month);
        
        if (availableTargetNames.contains(tableName)) {
            return tableName;
        } else {
            throw new IllegalArgumentException("无法路由到表: " + tableName);
        }
    }

    // 范围查询时使用(如 BETWEEN)
    @Override
    public Collection<String> doSharding(Collection<String> availableTargetNames,
                                        RangeShardingValue<Comparable<?>> shardingValue) {
        Collection<String> result = new LinkedHashSet<>();
        LocalDate now = LocalDate.now();
        int currentYear = now.getYear();
        
        // 简化处理:假设只查最近3个月
        for (int i = 0; i < 3; i++) {
            LocalDate date = now.minusMonths(i);
            String tableName = String.format("order_%d%02d", date.getYear(), date.getMonthValue());
            if (availableTargetNames.contains(tableName)) {
                result.add(tableName);
            }
        }
        return result;
    }

    @Override
    public void init() {}

    @Override
    public String getType() {
        return "ORDER_BY_MONTH";
    }

    @Override
    public void setProps(Properties props) {
        this.props = props;
    }

    @Override
    public Properties getProps() {
        return props;
    }
}

步骤 6:创建 Mapper 接口(MyBatis-Plus)

@Mapper
public interface OrderMapper extends BaseMapper<Order> {
    // 可继承 BaseMapper 所有方法
}

步骤 7:编写 Service 测试分表

@Service
@Transactional
@Slf4j
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    public void createOrder(Long userId, BigDecimal amount) {
        Order order = new Order();
        order.setUserId(userId);
        order.setAmount(amount);
        // createTime 由 MetaObjectHandler 自动填充
        orderMapper.insert(order);
        log.info("订单插入成功,ID={}", order.getId());
    }

    public List<Order> getOrdersByUser(Long userId) {
        return orderMapper.selectList(
            new LambdaQueryWrapper<Order>().eq(Order::getUserId, userId)
        );
    }
}

步骤 8:启用自动填充(可选但推荐)

@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
        this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
    }
}

三、常见错误与解决方案

错误现象 原因 解决方案
Table 'order' doesn't exist 未配置分片规则或表名错误 检查 actual-data-nodes 和逻辑表名
插入时报 sharding value is null 分片列(如 create_time)为空 确保插入前已设置分片键
查询不走分表 使用了非分片列查询 尽量用分片列作为查询条件
启动报 ClassNotFoundException 缺少依赖或版本不兼容 升级 ShardingSphere 到 5.x+
跨月查询失败 doSharding 返回空 实现 RangeShardingValue 支持范围查询

四、注意事项

  1. 分片键一旦确定,不可更改:如 create_time 必须在插入时就确定。
  2. 避免跨分片复杂查询JOINGROUP BYDISTINCT 在跨表时性能差。
  3. 分页查询需合并结果:ShardingSphere 会从多个表查数据再内存排序分页,大数据量慎用。
  4. 主键冲突风险:建议使用 分布式 ID(如雪花算法)而非自增主键。
  5. DDL 操作困难:修改表结构需同步所有分表,建议使用 Liquibase/Flyway 管理。

五、使用技巧

1. 动态创建分表(自动建表)

可在系统启动时检查并自动创建未来几个月的表:

@PostConstruct
public void createTablesIfNotExists() {
    // 检查 order_202503 是否存在,不存在则创建
}

2. 使用 Hint 强制路由(调试用)

// 强制路由到 order_202501
HintManager hintManager = HintManager.getInstance();
hintManager.addTableShardingValue("order", "order_202501");

3. 结合 MyBatis-Plus 分页

Page<Order> page = new Page<>(1, 10);
Page<Order> result = orderMapper.selectPage(page, null);
// ShardingSphere 会自动处理跨表分页

六、最佳实践与性能优化

✅ 最佳实践

实践 说明
选择合适分片键 高频查询字段优先,如 create_time, user_id
预创建分表 提前创建未来 3~6 个月的表,避免运行时创建失败
使用分布式 ID 避免自增主键冲突,推荐 IdWorkerLeaf
冷热数据分离 历史表可归档到低性能存储
监控分片状态 记录各表数据量、查询延迟

⚡ 性能优化建议

  1. 索引优化

    • 每个分表都需建立相同索引
    • 分片键建议作为联合索引前缀:(create_time, user_id)
  2. 批量插入优化

    // 使用 MyBatis-Plus 批量插入
    service.saveBatch(orders, 1000);
    
  3. 连接池配置

    • 增大连接池大小(因可能并发访问多张表)
  4. 避免全路由查询

    • 查询尽量带上分片键,避免 SELECT * FROM order 扫描所有表
  5. 读写分离 + 分表

    • 可结合 readwrite-splitting 实现读写分离,进一步提升性能

七、高级场景

场景 1:按用户 ID 取模分表

# 配置
actual-data-nodes: ds0.order_${0..9}
sharding-column: user_id
algorithmClassName: com.example.sharding.ModShardingAlgorithm
// 算法
String tableName = "order_" + (userId % 10);

场景 2:分库 + 分表

actual-data-nodes: ds${0..1}.order_${0..9}
# 2 个库,每个库 10 张表,共 20 张表

八、验证是否生效

  1. 查看 SQL 日志

    INSERT INTO order_202501 (...) VALUES (...)
    

    如果看到 order_202501 而不是 order,说明分表成功。

  2. 测试跨月插入

    order.setCreateTime(LocalDateTime.of(2025, 1, 15, 10, 0));
    // 应插入 order_202501
    
    order.setCreateTime(LocalDateTime.of(2025, 2, 15, 10, 0));
    // 应插入 order_202502
    

总结

MyBatis-Plus 本身不支持分表,但通过 ShardingSphere-JDBC 可完美集成,实现透明分表。

🔑 核心要点:

  1. ✅ 使用 shardingsphere-jdbc-core-spring-boot-starter
  2. ✅ 配置 actual-data-nodes 和分片算法
  3. ✅ 实现 StandardShardingAlgorithm
  4. ✅ 实体类使用逻辑表名
  5. ✅ 避免跨分片复杂查询

掌握这套方案,你就可以轻松应对亿级数据量的系统架构挑战!