1. 为什么需要分库分表?
在互联网应用快速发展的今天,数据量呈现爆炸式增长。我清楚地记得去年接手的一个电商项目,订单表数据量已经突破5000万条,查询响应时间从最初的200ms逐渐恶化到2s以上。这就是典型的数据量增长带来的性能瓶颈。
1.1 单库单表的性能天花板
当MySQL单表数据量超过500万行时,就会开始出现明显的性能下降。这主要源于几个方面:
- 索引效率降低:B+树索引的层级会随着数据量增加而变深,导致查询时需要更多的磁盘I/O
- 锁竞争加剧:大量并发操作同一张表时,锁等待时间显著增加
- 维护成本高:备份恢复、DDL操作耗时成倍增长
提示:不要等到系统出现性能问题才考虑分库分表,应该在设计初期就评估数据增长趋势,提前规划分片策略。
1.2 分库分表的本质
分库分表的核心思想是"分而治之",通过将数据分散到不同的数据库实例和表中,实现:
- 水平扩展:突破单机存储和计算能力的限制
- 性能提升:减少单表数据量,提高查询效率
- 高可用性:避免单点故障,提高系统容错能力
在实际项目中,我们通常采用水平分片(Horizontal Partitioning)的方式,按照某个分片键(如用户ID、订单ID)将数据分散存储。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Sharding-JDBC核心原理解析
2.1 Sharding-JDBC的架构定位
Sharding-JDBC是Apache ShardingSphere的第一个产品,定位为轻量级的Java框架。与常见的中间件不同,它采用客户端直连模式,无需额外部署代理服务。
java复制// 典型的数据源配置
@Bean
public DataSource dataSource() throws SQLException {
// 配置真实数据源
Map<String, DataSource> dataSourceMap = new HashMap<>();
// 配置第一个数据源
BasicDataSource dataSource1 = new BasicDataSource();
dataSource1.setDriverClassName("com.mysql.jdbc.Driver");
dataSource1.setUrl("jdbc:mysql://localhost:3306/ds0");
dataSource1.setUsername("root");
dataSource1.setPassword("");
dataSourceMap.put("ds0", dataSource1);
// 配置分片规则
ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
// ... 分片规则配置
return ShardingDataSourceFactory.createDataSource(dataSourceMap, shardingRuleConfig, new Properties());
}
2.2 SQL解析与路由过程
Sharding-JDBC的执行流程可以分为以下几个关键步骤:
- SQL解析:使用ANTLR将SQL解析为抽象语法树(AST)
- 查询优化:对SQL进行改写和优化
- SQL路由:根据分片策略确定数据节点
- 结果归并:将多个数据节点的结果合并返回
这个过程中最复杂的是SQL改写阶段,需要处理各种场景:
- 分页查询的LIMIT改写
- 聚合函数的计算优化
- 分布式主键生成
- 分布式事务处理
2.3 分片策略详解
Sharding-JDBC提供了4种标准分片策略:
| 策略类型 | 适用场景 | 优缺点 |
|---|---|---|
| 标准分片 | 等值查询 | 实现简单,范围查询效率低 |
| 复合分片 | 多条件查询 | 灵活度高,配置复杂 |
| 行表达式 | 简单规则 | 配置简洁,功能有限 |
| Hint强制 | 特殊路由 | 完全控制,侵入性强 |
在实际项目中,我推荐使用标准分片策略配合行表达式,既保证灵活性又降低复杂度:
yaml复制spring:
shardingsphere:
sharding:
tables:
t_order:
actual-data-nodes: ds$->{0..1}.t_order_$->{0..15}
table-strategy:
inline:
sharding-column: order_id
algorithm-expression: t_order_$->{order_id % 16}
3. Spring Boot集成实战
3.1 环境准备与依赖配置
首先在pom.xml中添加必要依赖:
xml复制<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>5.1.1</version>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
注意:Sharding-JDBC 5.x版本对Spring Boot的支持更加完善,但要注意与Spring Boot版本的兼容性。Spring Boot 2.5.x建议使用ShardingSphere 5.1.x。
3.2 数据源与分片规则配置
在application.yml中配置多数据源和分片规则:
yaml复制spring:
shardingsphere:
datasource:
names: ds0,ds1
ds0:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/ds0
username: root
password:
ds1:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/ds1
username: root
password:
sharding:
tables:
t_order:
actual-data-nodes: ds$->{0..1}.t_order_$->{0..1}
database-strategy:
inline:
sharding-column: user_id
algorithm-expression: ds$->{user_id % 2}
table-strategy:
inline:
sharding-column: order_id
algorithm-expression: t_order_$->{order_id % 2}
3.3 分布式主键生成策略
在分库分表环境下,传统的自增ID会导致主键冲突。Sharding-JDBC提供了多种分布式ID生成器:
java复制public class Order {
@Id
@GeneratedValue(generator = "snowflake")
@GenericGenerator(name = "snowflake", strategy = "com.example.config.SnowflakeIdGenerator")
private Long orderId;
// 其他字段
}
我推荐使用雪花算法(Snowflake),它生成的ID具有以下特点:
- 64位长整型
- 包含时间戳、工作节点ID和序列号
- 全局唯一且趋势递增
4. 生产环境中的关键问题
4.1 分布式事务处理
分库分表后,跨库事务成为难题。Sharding-JDBC支持以下几种方案:
- XA事务:强一致性,但性能较差
- Seata AT模式:阿里开源的分布式事务解决方案
- Saga模式:最终一致性,适合长事务
- 本地事务+消息队列:柔性事务的典型实现
对于大多数业务场景,我建议采用本地消息表+定时任务的方案:
java复制@Transactional
public void createOrder(Order order) {
// 1. 保存订单
orderMapper.insert(order);
// 2. 记录本地消息
MessageRecord record = new MessageRecord();
record.setContent(JSON.toJSONString(order));
messageMapper.insert(record);
// 3. 发送MQ消息(可选)
// rocketMQTemplate.send(...);
}
4.2 跨库查询与JOIN优化
分库分表后,跨库JOIN变得困难。常见的解决方案包括:
- 冗余字段:在相关表中冗余常用查询字段
- 广播表:将小表复制到所有分片
- 绑定表:将关联表使用相同的分片规则
- 内存计算:先查询再在内存中关联
yaml复制spring:
shardingsphere:
sharding:
binding-tables:
- t_order,t_order_item
broadcast-tables:
- t_region
4.3 扩容与数据迁移
随着业务增长,可能需要增加分片数量。这时需要考虑数据迁移方案:
- 停机迁移:简单直接,但影响业务
- 双写迁移:新旧库同时写入,逐步切换
- 增量同步:基于binlog的实时同步
我最近实施的一个迁移方案如下:
- 新库配置为只读
- 使用DataX全量同步历史数据
- 配置Canal增量同步变更
- 切换应用配置并验证
- 旧库下线
5. 性能优化与监控
5.1 SQL优化建议
在分库分表环境下,SQL编写需要特别注意:
- 避免使用SELECT *,明确指定列名
- 分页查询使用LIMIT配合分片键
- 减少跨分片操作
- 合理使用绑定表
java复制// 不推荐的写法
@Select("SELECT * FROM t_order WHERE status = #{status}")
List<Order> findByStatus(@Param("status") int status);
// 推荐的写法
@Select("SELECT order_id, user_id, amount FROM t_order WHERE user_id = #{userId} AND status = #{status}")
List<Order> findByUserAndStatus(@Param("userId") long userId, @Param("status") int status);
5.2 监控与调优
ShardingSphere提供了完善的监控指标,可以通过Prometheus采集:
yaml复制spring:
shardingsphere:
metrics:
enabled: true
name: sharding_demo
prometheus:
host: 0.0.0.0
port: 9090
关键监控指标包括:
- 分片执行耗时
- SQL执行次数
- 连接池状态
- 分布式事务状态
我在实际项目中总结的调优经验:
- 分片数建议为2的N次方,便于后续扩容
- 连接池大小建议设置为(核心数 * 2) + 有效磁盘数
- 定期检查慢查询日志
- 避免热点数据集中在一个分片
6. 完整代码示例
以下是一个完整的Spring Boot + Sharding-JDBC集成示例:
- 项目结构:
code复制src/main/java
├── com.example
│ ├── config
│ │ ├── ShardingConfig.java
│ │ └── SnowflakeIdGenerator.java
│ ├── controller
│ │ └── OrderController.java
│ ├── entity
│ │ └── Order.java
│ ├── mapper
│ │ └── OrderMapper.java
│ └── service
│ └── OrderService.java
src/main/resources
├── application.yml
└── mapper
└── OrderMapper.xml
- 核心配置类:
java复制@Configuration
public class ShardingConfig {
@Bean
public DataSource dataSource() throws SQLException {
// 数据源配置
Map<String, DataSource> dataSourceMap = new HashMap<>();
// 配置分片规则
ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
shardingRuleConfig.getTableRuleConfigs().add(getOrderTableRuleConfiguration());
// 创建ShardingDataSource
return ShardingDataSourceFactory.createDataSource(dataSourceMap, shardingRuleConfig, new Properties());
}
private TableRuleConfiguration getOrderTableRuleConfiguration() {
TableRuleConfiguration result = new TableRuleConfiguration("t_order", "ds$->{0..1}.t_order_$->{0..1}");
result.setDatabaseShardingStrategyConfig(new InlineShardingStrategyConfiguration("user_id", "ds$->{user_id % 2}"));
result.setTableShardingStrategyConfig(new InlineShardingStrategyConfiguration("order_id", "t_order_$->{order_id % 2}"));
return result;
}
}
- 服务层实现:
java复制@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Transactional
public Long createOrder(Order order) {
orderMapper.insert(order);
return order.getOrderId();
}
public List<Order> getOrdersByUser(Long userId) {
return orderMapper.selectByUserId(userId);
}
}
在实际开发中,我发现几个容易出错的地方:
- 分片键的选择非常重要,应该选择分布均匀且查询频繁的字段
- 避免在事务中执行跨分片操作,会导致事务时间过长
- 批量插入操作需要特别注意,建议使用VALUES方式而非多次INSERT
