为什么需要分库分表?
当单表数据量超过千万级,数据库的写入和查询性能会急剧下降。索引膨胀、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,使两者都能路由到同一分片。
七、总结
分库分表是应对海量数据的有效手段,但绝非银弹。设计时需重点考虑:
- 分片键选择:高频、均匀、避免热点。
- 路由策略:尽量带分片键查询。
- 全局 ID:雪花算法或号段模式。
- 分布式事务:根据一致性要求选择方案。
- 扩容与运维:提前规划,避免后期迁移痛苦。
下一步可以学习 ShardingSphere 的读写分离、数据加密、影子库等功能,构建更完善的分布式数据库架构。
—
延伸阅读:
- Apache ShardingSphere 官方文档
- 《数据分片与路由》—— 分布式数据库核心原理
- Seata 官方文档:AT 模式详解