1. 分库分表场景下的数据源管理困境
在分布式系统架构中,当单库单表的性能成为瓶颈时,分库分表是常见的解决方案。但随之而来的数据源管理问题却让很多开发者头疼——如何在同一个应用中同时使用ShardingSphere这类分库分表中间件和基于@DS注解的多数据源方案?
我去年在重构一个电商订单系统时就遇到了这个典型场景。订单表已经超过5000万行,急需分表;同时系统还需要访问独立的用户库、商品库和日志库。经过多次踩坑和验证,最终找到了稳定可靠的整合方案。下面就把这套实战经验完整分享给大家。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心组件工作原理解析
2.1 ShardingSphere 的运作机制
ShardingSphere通过改写SQL语句来实现分片路由。以5.2.1版本为例,其核心流程是:
- SQL解析:通过ANTLR将SQL解析为抽象语法树(AST)
- 路由计算:根据分片键值计算目标数据节点
- SQL改写:将原始SQL改写为分片SQL(如
order_0、order_1) - 执行引擎:通过多线程执行器并发操作多个真实数据源
- 结果归并:对跨分片查询结果进行聚合
关键点在于它通过DataSource接口的包装类控制整个流程,这意味着:
java复制// 伪代码展示ShardingSphere数据源结构
ShardingSphereDataSource {
Map<String, DataSource> actualDataSources // 真实物理数据源
ShardingRule shardingRule // 分片规则配置
}
2.2 @DS 注解的实现原理
以MyBatis-Plus的多数据源方案为例,@DS注解本质是通过AOP动态切换DataSource:
java复制@Around("@annotation(ds)")
public Object around(ProceedingJoinPoint point, DS ds) {
String dsKey = ds.value();
DynamicDataSourceContextHolder.push(dsKey); // 切换数据源
try {
return point.proceed();
} finally {
DynamicDataSourceContextHolder.poll(); // 恢复数据源
}
}
这种方案依赖线程上下文(ThreadLocal)保存当前数据源标识,与ShardingSphere的运作层级存在冲突。
3. 整合方案设计与实现
3.1 架构分层设计
通过分层设计解决组件冲突:
code复制[应用层]
│
├── @DS注解路由 (动态数据源层)
│ │
│ └── ShardingSphereDataSource (分片数据源)
│ │
│ ├── MasterSlaveDataSource (读写分离数据源)
│ │ │
│ │ └── DruidDataSource (物理连接池)
│ │
│ └── EncryptDataSource (数据加密数据源)
│
└── 普通DataSource (非分片数据源)
关键实现步骤:
- 排除ShardingSphere的自动配置
- 手动构建ShardingSphere数据源
- 将ShardingDataSource注册为动态数据源的一个实例
3.2 具体配置示例
yaml复制# 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://db1:3306/order_db
username: root
password: 123456
ds1:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://db2:3306/order_db
username: root
password: 123456
rules:
sharding:
tables:
t_order:
actual-data-nodes: ds$->{0..1}.t_order_$->{0..15}
database-strategy:
standard:
sharding-column: user_id
precise-algorithm-class-name: com.example.DbShardingAlgorithm
table-strategy:
standard:
sharding-column: order_id
precise-algorithm-class-name: com.example.TableShardingAlgorithm
对应的Java配置类:
java复制@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.shardingsphere")
public DataSource shardingDataSource() throws SQLException {
return ShardingSphereDataSourceFactory.createDataSource(
new ShardingSphereRuleConfiguration(),
new Properties());
}
@Bean
@Primary
public DataSource dynamicDataSource(DataSource shardingDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("sharding", shardingDataSource);
// 添加其他普通数据源
targetDataSources.put("product", productDataSource());
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setTargetDataSources(targetDataSources);
dynamicDataSource.setDefaultTargetDataSource(shardingDataSource);
return dynamicDataSource;
}
}
4. 关键问题与解决方案
4.1 事务一致性难题
当@DS切换到非ShardingSphere数据源时,原生分布式事务会失效。解决方案:
- 使用Seata作为全局事务管理器
- 配置
@GlobalTransactional注解覆盖本地事务
java复制@Service
public class OrderService {
@DS("product") // 切换到商品库
@GlobalTransactional // 覆盖为全局事务
public void createOrder(OrderDTO order) {
productMapper.reduceStock(order.getProductId());
orderMapper.insert(order); // 该方法使用ShardingSphere数据源
}
}
4.2 分片键丢失问题
在动态数据源切换时,可能导致ShardingSphere无法获取分片键。需要在切换前显式设置:
java复制public class ShardingKeyContext {
private static final ThreadLocal<Map<String, Object>> context = new ThreadLocal<>();
public static void setShardingKey(String column, Object value) {
Map<String, Object> map = context.get();
if (map == null) {
map = new HashMap<>();
context.set(map);
}
map.put(column, value);
}
}
// 在AOP切面中处理
@Around("@annotation(ds)")
public Object around(ProceedingJoinPoint point, DS ds) {
if("sharding".equals(ds.value())) {
Object arg = point.getArgs()[0];
if(arg instanceof Order) {
ShardingKeyContext.setShardingKey("user_id", ((Order)arg).getUserId());
}
}
// ...原有切换逻辑
}
5. 性能优化实践
5.1 连接池配置建议
由于存在多层数据源包装,连接数需要特别规划:
code复制实际最大连接数 = DynamicDataSource.maxActive ×
ShardingSphereDataSource.actualDataSources.size ×
物理数据源.maxPoolSize
推荐配置方案:
yaml复制# HikariCP配置示例
shardingsphere:
datasource:
ds0:
hikari:
maximum-pool-size: 5 # 每个物理库连接数
dynamic:
datasource:
hikari:
max-active: 3 # 动态数据源层连接数
5.2 监控方案
通过Prometheus监控各层数据源状态:
java复制@Bean
public CollectorRegistry metricsRegistry(DataSource dataSource) {
HikariDataSource hikariDataSource = unwrapDataSource(dataSource);
HikariPoolMXBean poolProxy = hikariDataSource.getHikariPoolMXBean();
Gauge.builder("datasource.active.connections", poolProxy::getActiveConnections)
.tag("type", "sharding")
.register(Metrics.globalRegistry);
}
6. 典型业务场景实现
6.1 跨库关联查询方案
当需要关联查询分片表和非分片表时:
sql复制/* 错误示例(会导致全路由) */
SELECT o.*, p.name
FROM t_order o LEFT JOIN product p ON o.product_id = p.id
WHERE o.user_id = 123;
/* 正确方案 */
// 1. 先查产品信息
@DS("product")
List<Product> products = productMapper.selectByIds(orderIds);
// 2. 再查订单信息
List<Order> orders = orderMapper.selectByUser(userId);
// 3. 内存关联
orders.forEach(o ->
o.setProductName(products.stream()
.filter(p -> p.getId().equals(o.getProductId()))
.findFirst()
.map(Product::getName)
.orElse(null))
);
6.2 分布式ID生成策略
避免使用数据库自增ID,推荐方案:
java复制public class SnowflakeKeyGenerator implements ShardingKeyGenerator {
private final Snowflake snowflake = new Snowflake(workerId);
@Override
public Comparable<?> generateKey() {
return snowflake.nextId();
}
}
// 在分片配置中指定
spring.shardingsphere.rules.sharding.tables.t_order.key-generate-strategy.column=order_id
spring.shardingsphere.rules.sharding.tables.t_order.key-generate-strategy.key-generator-name=snowflake
7. 升级与迁移注意事项
7.1 版本兼容性矩阵
| ShardingSphere版本 | MyBatis-Plus版本 | 注意事项 |
|---|---|---|
| 5.x | 3.5.x+ | 推荐组合 |
| 4.x | 3.4.x | 需排除旧版SpringBoot自动配置 |
| 3.x | 不推荐 | 存在AOP顺序问题 |
7.2 灰度迁移方案
对于存量系统的迁移建议:
- 先引入动态数据源,保持原有单库模式
- 逐步将只读业务切换到分片数据源
- 最后迁移写操作
- 使用双写方案过渡(需处理数据一致性问题):
java复制public void createOrder(Order order) {
// 写入新分片库
shardingOrderMapper.insert(order);
// 同步写入旧库(异步队列更佳)
try {
legacyOrderMapper.insert(order);
} catch (Exception e) {
log.error("双写失败", e);
// 记录补偿任务
}
}
这套方案在百万级QPS的生产环境中验证通过,关键是要理解各层数据源的运作机制,做好事务边界控制。实际落地时建议先在测试环境充分验证,特别是异常场景下的数据一致性表现。
