为什么需要分库分表?
当单表数据量超过 500 万行或单库磁盘容量、连接数达到瓶颈时,查询性能会急剧下降。此时垂直拆分(按业务拆库)和水平拆分(按规则拆表)成为必选项。但分库分表也引入了分布式事务、跨节点查询、全局 ID 等复杂问题。本文将带你从零设计一个订单分库分表方案,并基于 ShardingSphere-JDBC 实现。
一、分库分表策略选择
1.1 垂直拆分 vs 水平拆分
- 垂直分库:按业务将不同表拆分到不同数据库,例如用户库、订单库、商品库。解决的是表数量多、IO 竞争问题。
- 垂直分表:将宽表拆分为主表+扩展表,例如订单基础信息与订单详情。
- 水平分库:同一张表按规则拆分到多个数据库。
- 水平分表:同一张表按规则拆分为多张表,仍在同一库。
最佳实践:优先垂直拆分,再水平拆分;水平拆分时通常分库与分表同时进行,避免单库表过多。
1.2 分片键与分片算法
分片键(Sharding Key)是数据分布的依据,必须选择高基数、分布均匀、且高频查询条件的字段。订单场景常用 userid 或 orderid。
- 取模:
user_id % 4,分布均匀但扩容困难。 - 范围:按时间范围分片,易扩容但可能热点。
- 一致性哈希:扩容时影响小,但实现复杂。
注意:一旦选定分片键,后续所有查询尽量携带该字段,否则会全路由扫描。
二、实战:基于 ShardingSphere-JDBC 拆分订单表
2.1 环境准备
- MySQL 8.0(两个库:
ds0、ds1) - Spring Boot 2.7.x
- ShardingSphere-JDBC 5.3.0
创建数据库与表:
-- 在 ds0 和 ds1 中分别执行
CREATE DATABASE ds0;
CREATE DATABASE ds1;
-- ds0 中创建 t_order_0, t_order_1
CREATE TABLE ds0.t_order_0 (
order_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
amount DECIMAL(10,2),
create_time DATETIME,
PRIMARY KEY (order_id)
);
CREATE TABLE ds0.t_order_1 LIKE ds0.t_order_0;
-- ds1 中创建 t_order_0, t_order_1
CREATE TABLE ds1.t_order_0 LIKE ds0.t_order_0;
CREATE TABLE ds1.t_order_1 LIKE ds0.t_order_0;
2.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>
2.3 配置分片规则
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://localhost:3306/ds0?useSSL=false
username: root
password: 123456
ds1:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/ds1?useSSL=false
username: root
password: 123456
rules:
sharding:
tables:
t_order:
actual-data-nodes: ds$->{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: ds$->{user_id % 2}
table-inline:
type: INLINE
props:
algorithm-expression: t_order_$->{user_id % 2}
props:
sql-show: true
注意:actual-data-nodes 必须覆盖所有真实表,否则插入时找不到表。
2.4 编写实体与 Mapper
@Data
@TableName("t_order")
public class Order {
@TableId(type = IdType.ASSIGN_ID)
private Long orderId;
private Long userId;
private BigDecimal amount;
private LocalDateTime createTime;
}
@Mapper
public interface OrderMapper extends BaseMapper<Order> {
}
2.5 测试分片效果
@SpringBootTest
class OrderShardingTest {
@Autowired
private OrderMapper orderMapper;
@Test
void testInsert() {
for (long userId = 1; userId <= 4; userId++) {
Order order = new Order();
order.setUserId(userId);
order.setAmount(new BigDecimal("100.00"));
order.setCreateTime(LocalDateTime.now());
orderMapper.insert(order);
}
}
@Test
void testSelect() {
// 携带分片键,精准路由
Order order = orderMapper.selectById(1L);
System.out.println(order);
}
}
执行 testInsert 后,观察日志:userid=1 会路由到 ds1.torder1(1%2=1),userid=2 路由到 ds0.torder0。数据均匀分散。
三、常见坑与解决方案
3.1 分布式主键
分库分表后不能依赖数据库自增。ShardingSphere 内置雪花算法,通过 @TableId(type = IdType.ASSIGN_ID) 自动生成 19 位 Long 型 ID。
3.2 跨节点分页
LIMIT 10 OFFSET 100 会从所有分片取 110 条再内存归并,性能差。建议:避免深分页,或使用“上次最大 ID”作为游标。
3.3 分布式事务
跨库更新需引入 Seata 或使用柔性事务。ShardingSphere 支持 LOCAL、XA、BASE 事务,生产环境推荐 BASE + 消息最终一致。
3.4 数据迁移与扩容
从单库迁移到分库分表时,可使用 ShardingSphere-Scaling 或双写方案。扩容时建议成倍增加分片(如 2 库变 4 库),减少数据迁移量。
四、总结与延伸
分库分表是解决海量数据存储与高并发的有效手段,但会带来复杂性。核心步骤:
- 评估是否必须分(单表 < 500 万优先优化索引、归档)。
- 选择合适分片键与算法,确保均匀且高频查询携带。
- 使用 ShardingSphere-JDBC 透明化分片,减少侵入。
- 处理好全局 ID、跨节点查询、事务。
下一步:可研究 ShardingSphere-Proxy(代理层)、读写分离、弹性伸缩,以及结合 NewSQL 如 TiDB。
> 本文代码已上传至 GitHub 示例仓库,欢迎实践。