一、核心概念
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 支持范围查询 |
四、注意事项
- 分片键一旦确定,不可更改:如
create_time
必须在插入时就确定。 - 避免跨分片复杂查询:
JOIN
、GROUP BY
、DISTINCT
在跨表时性能差。 - 分页查询需合并结果:ShardingSphere 会从多个表查数据再内存排序分页,大数据量慎用。
- 主键冲突风险:建议使用 分布式 ID(如雪花算法)而非自增主键。
- 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 | 避免自增主键冲突,推荐 IdWorker 或 Leaf |
冷热数据分离 | 历史表可归档到低性能存储 |
监控分片状态 | 记录各表数据量、查询延迟 |
⚡ 性能优化建议
索引优化
- 每个分表都需建立相同索引
- 分片键建议作为联合索引前缀:
(create_time, user_id)
批量插入优化
// 使用 MyBatis-Plus 批量插入 service.saveBatch(orders, 1000);
连接池配置
- 增大连接池大小(因可能并发访问多张表)
避免全路由查询
- 查询尽量带上分片键,避免
SELECT * FROM order
扫描所有表
- 查询尽量带上分片键,避免
读写分离 + 分表
- 可结合
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 张表
八、验证是否生效
查看 SQL 日志:
INSERT INTO order_202501 (...) VALUES (...)
如果看到
order_202501
而不是order
,说明分表成功。测试跨月插入:
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 可完美集成,实现透明分表。
🔑 核心要点:
- ✅ 使用
shardingsphere-jdbc-core-spring-boot-starter
- ✅ 配置
actual-data-nodes
和分片算法- ✅ 实现
StandardShardingAlgorithm
- ✅ 实体类使用逻辑表名
- ✅ 避免跨分片复杂查询
掌握这套方案,你就可以轻松应对亿级数据量的系统架构挑战!