Dynamic-datasource多数据源框架深度实践指南
多数据源框架深度解析指南,从原理到实践,从基础到进阶,一站式掌握Dynamic-datasource核心能力。
一、引言:为什么需要多数据源框架?
1.1 无多数据源框架时的痛点
在企业级应用开发中,多数据源场景日益普遍。在没有成熟框架支撑的情况下,开发者往往面临诸多困境:
代码耦合严重:数据源配置硬编码在代码中,切换需修改源码重新部署,难以独立演进。
手动切换繁琐:每次操作需手动获取Connection,代码冗余且易出错,忘记切换会导致数据写入错误库表。
事务管理复杂:跨数据源事务需手动管理,Spring的@Transactional注解失效,需自行处理事务边界和回滚逻辑。
跨数据源事务需要手动管理,Spring的@Transactional注解在多数据源场景下失效,开发者需要自行处理事务边界、回滚逻辑,代码复杂度急剧上升。
可维护性困境
数据源切换逻辑散落在各处,新人难以理解;配置变更需要改动多处代码;测试困难,难以Mock数据源。
1.2 典型业务场景全景
多数据源框架在以下场景中具有不可替代的价值:
| 场景类型 | 场景描述 | 数据源特点 |
|---|---|---|
| 读写分离 | 主库写入,从库读取,提升系统读性能 | 一主多从,读负载均衡 |
| 多租户架构 | 每个租户独立数据库,数据物理隔离 | 动态数据源,按租户切换 |
| 系统整合/迁移 | 新旧系统并行,逐步迁移数据 | 多业务库,跨库查询 |
| 跨业务线交互 | 不同业务线独立数据库,需数据互通 | 多业务库,按需切换 |
| 分库分表 | 数据量大,单库性能瓶颈 | 多分片库,路由规则 |
二、方案对比:多数据源解决方案选型
2.1 Spring原生AbstractRoutingDataSource
Spring提供了AbstractRoutingDataSource抽象类,是实现多数据源的基础方案。内部维护数据源Map,通过determineCurrentLookupKey()决定使用哪个数据源。
核心实现要点:需自行实现DataSourceContextHolder(ThreadLocal存储)、DynamicDataSource(路由决策)、自定义注解、AOP切面等多个组件。
优缺点分析
| 优点 | 缺点 |
|---|---|
| 无需引入第三方依赖 | 需要自行实现注解、切面、上下文管理 |
| 完全可控,灵活性高 | 功能单一,缺乏动态数据源管理能力 |
| 学习成本低 | 无连接池监控、无负载均衡等高级特性 |
| Spring原生支持 | 事务管理需自行处理 |
2.2 ShardingSphere
ShardingSphere是Apache顶级项目,提供数据分片、读写分离等分布式数据库解决方案。
核心能力
- 数据分片:支持分库分表,提供多种分片策略
- 读写分离:支持一主多从,自动路由
- 弹性伸缩:支持数据迁移、扩缩容
- 数据加密:敏感数据自动加密解密
配置示例
spring:
shardingsphere:
datasource:
names: ds-master,ds-slave
ds-master:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/master_db
username: root
password: root
ds-slave:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/slave_db
username: root
password: root
rules:
readwrite-splitting:
data-sources:
myds:
write-data-source-name: ds-master
read-data-source-names:
- ds-slave
load-balancer-name: round_robin
load-balancers:
round_robin:
type: ROUND_ROBIN
适用场景与局限性
| 适用场景 | 局限性 |
|---|---|
| 大规模数据分片 | 配置复杂,学习成本高 |
| 复杂的读写分离需求 | 对简单多数据源场景过于重量级 |
| 分布式数据库中间件需求 | 与MyBatis-Plus等框架整合可能存在兼容问题 |
2.3 Dynamic-datasource
Dynamic-datasource是Baomidou团队开源的多数据源框架,与MyBatis-Plus生态无缝集成。
框架定位
专注于多数据源切换,提供简洁的API和强大的扩展能力,是"小而美"的解决方案。
核心优势
- 开箱即用:一个注解
@DS即可切换数据源 - 功能丰富:支持动态添加/移除数据源、读写分离、负载均衡
- 生态融合:与MyBatis-Plus、Seata等无缝集成
- 轻量级:核心功能简洁,无侵入式设计
- 社区活跃:Baomidou团队维护,文档完善
2.4 方案对比总结
| 维度 | AbstractRoutingDataSource | ShardingSphere | Dynamic-datasource |
|---|---|---|---|
| 实现成本 | 高(需自行实现) | 中(配置复杂) | 低(开箱即用) |
| 功能丰富度 | 低 | 高 | 中 |
| 学习成本 | 低 | 高 | 低 |
| 灵活性 | 高 | 中 | 高 |
| 事务支持 | 需自行处理 | 支持XA/本地事务 | 支持本地事务 |
| 动态数据源 | 需自行实现 | 支持 | 原生支持 |
| 读写分离 | 需自行实现 | 原生支持 | 原生支持 |
| 适用场景 | 简单多数据源 | 大规模分片 | 中小规模多数据源 |
选型建议
- 简单的多数据源切换,追求轻量级:Dynamic-datasource
- 大规模数据分片、分布式数据库需求:ShardingSphere
- 对框架有深度定制需求、不愿引入第三方依赖:AbstractRoutingDataSource
三、框架认知:Dynamic-datasource核心原理
3.1 整体架构设计
Dynamic-datasource的核心架构围绕DataSource抽象和上下文隔离两大机制设计。
核心组件关系
┌─────────────────────────────────────────────────────────────┐
│ Spring Application │
├─────────────────────────────────────────────────────────────┤
│ @DS("xxx") ──> DynamicDataSourceAspect ──> 切面织入 │
│ │
│ ↓ │
│ │
│ DynamicDataSourceContextHolder (ThreadLocal) │
│ ↓ │
│ DynamicRoutingDataSource │
│ (继承 AbstractDataSource) │
│ ↓ │
│ ┌────────────────┼────────────────┐ │
│ ↓ ↓ ↓ │
│ DataSource_1 DataSource_2 DataSource_N │
│ (master) (slave) (other) │
└─────────────────────────────────────────────────────────────┘
DataSource抽象层
框架通过继承AbstractDataSource实现DynamicRoutingDataSource,作为Spring容器的Primary DataSource。所有数据库操作都经过这一层路由到具体的物理数据源。
3.2 核心机制解析
ThreadLocal上下文隔离
框架使用ThreadLocal存储当前线程的数据源标识,确保每个线程的数据源切换互不干扰:
// 核心实现原理(简化版)
public class DynamicDataSourceContextHolder {
// 使用ThreadLocal存储数据源名称
private static final ThreadLocal<String> LOOKUP_KEY_HOLDER = new ThreadLocal<>();
// 设置当前数据源
public static void push(String ds) {
LOOKUP_KEY_HOLDER.set(ds);
}
// 获取当前数据源
public static String peek() {
return LOOKUP_KEY_HOLDER.get();
}
// 清除数据源(恢复默认)
public static void poll() {
LOOKUP_KEY_HOLDER.remove();
}
}
AOP切面实现原理
框架通过@DS注解配合AOP切面,在方法执行前设置数据源,执行后清理:
// 切面核心逻辑(简化版)
@Aspect
public class DynamicDataSourceAspect {
@Around("@annotation(ds)")
public Object around(ProceedingJoinPoint point, DS ds) throws Throwable {
String dsName = ds.value();
try {
// 方法执行前:设置数据源
DynamicDataSourceContextHolder.push(dsName);
// 执行目标方法
return point.proceed();
} finally {
// 方法执行后:清理数据源
DynamicDataSourceContextHolder.poll();
}
}
}
动态路由决策链
当SQL执行时,路由决策流程如下:
- 检查当前线程
ThreadLocal中是否有数据源标识 - 如有,直接路由到对应数据源
- 如无,检查方法/类上是否有
@DS注解 - 如有注解,使用注解指定的数据源
- 如无注解,使用默认数据源(primary)
3.3 关键源码分析
DynamicRoutingDataSource核心逻辑
// 核心路由逻辑(简化版)
public class DynamicRoutingDataSource extends AbstractDataSource {
// 数据源Map:名称 -> DataSource实例
private Map<String, DataSource> dataSourceMap = new ConcurrentHashMap<>();
// 默认数据源名称
private String primary = "master";
@Override
public Connection getConnection() throws SQLException {
// 获取当前数据源
DataSource dataSource = determineDataSource();
return dataSource.getConnection();
}
// 决定使用哪个数据源
private DataSource determineDataSource() {
// 从ThreadLocal获取数据源名称
String lookupKey = DynamicDataSourceContextHolder.peek();
if (StringUtils.isEmpty(lookupKey)) {
lookupKey = this.primary;
}
DataSource dataSource = dataSourceMap.get(lookupKey);
if (dataSource == null) {
throw new IllegalStateException("数据源 [" + lookupKey + "] 不存在");
}
return dataSource;
}
// 动态添加数据源
public synchronized void addDataSource(String name, DataSource dataSource) {
dataSourceMap.put(name, dataSource);
}
// 动态移除数据源
public synchronized void removeDataSource(String name) {
dataSourceMap.remove(name);
}
}
@DS注解定义
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DS {
/**
* 数据源名称
* 可以是具体的名称,也可以是SpEL表达式
*/
String value() default "";
}
注解优先级处理
// 注解解析器(简化版)
public class DsProcessor {
// 方法级别注解 > 类级别注解 > 默认数据源
public String determineDs(MethodInvocation invocation) {
Method method = invocation.getMethod();
Class<?> targetClass = invocation.getThis().getClass();
// 1. 优先查找方法级别注解
DS methodDs = method.getAnnotation(DS.class);
if (methodDs != null) {
return methodDs.value();
}
// 2. 其次查找类级别注解
DS classDs = targetClass.getAnnotation(DS.class);
if (classDs != null) {
return classDs.value();
}
// 3. 返回null,使用默认数据源
return null;
}
}
四、基础实践:快速集成与使用
4.1 Maven依赖配置
<dependencies>
<!-- Dynamic-datasource 核心依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>4.3.0</version>
</dependency>
<!-- MyBatis-Plus(推荐搭配使用) -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.5</version>
</dependency>
<!-- 数据库驱动(以MySQL为例) -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- 连接池(HikariCP,Spring Boot默认包含) -->
<!-- 如需指定版本,可显式引入 -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>5.0.1</version>
</dependency>
</dependencies>
版本兼容说明
| Dynamic-datasource | Spring Boot | MyBatis-Plus |
|---|---|---|
| 4.3.0 | 3.x | 3.5.x |
| 4.2.0 | 2.7.x | 3.5.x |
| 3.6.1 | 2.3.x | 3.4.x |
4.2 YAML配置详解
基础配置示例
spring:
datasource:
dynamic:
# 主数据源名称(默认数据源)
primary: master
# 严格模式:未配置的数据源会抛出异常(生产环境建议true)
strict: true
# 数据源配置
datasource:
# 主库
master:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/master_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
# 订单库
order:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/order_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
# 库存库
inventory:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/inventory_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
连接池参数配置
spring:
datasource:
dynamic:
primary: master
datasource:
master:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/master_db
username: root
password: root
# HikariCP连接池配置
hikari:
# 最小空闲连接数
min-idle: 5
# 最大连接数
max-pool-size: 20
# 连接超时时间(毫秒)
connection-timeout: 30000
# 空闲连接存活最大时间(毫秒)
idle-timeout: 600000
# 连接最大存活时间(毫秒)
max-lifetime: 1800000
# 连接测试查询
connection-test-query: SELECT 1
order:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/order_db
username: root
password: root
hikari:
min-idle: 3
max-pool-size: 15
多环境配置示例
# application.yml(通用配置)
spring:
datasource:
dynamic:
primary: master
strict: false # 开发环境关闭严格模式
# application-dev.yml(开发环境)
spring:
datasource:
dynamic:
datasource:
master:
url: jdbc:mysql://localhost:3306/dev_master_db
username: dev_user
password: dev_pass
order:
url: jdbc:mysql://localhost:3306/dev_order_db
username: dev_user
password: dev_pass
# application-prod.yml(生产环境)
spring:
datasource:
dynamic:
strict: true # 生产环境开启严格模式
datasource:
master:
url: ${MASTER_DB_URL}
username: ${MASTER_DB_USERNAME}
password: ${MASTER_DB_PASSWORD}
hikari:
min-idle: 10
max-pool-size: 50
order:
url: ${ORDER_DB_URL}
username: ${ORDER_DB_USERNAME}
password: ${ORDER_DB_PASSWORD}
hikari:
min-idle: 5
max-pool-size: 30
4.3 @DS注解使用指南
导入注解:import com.baomidou.dynamic.datasource.annotation.DS;
注解优先级规则:方法级别 @DS > 类级别 @DS > 默认数据源(primary)
综合示例:
@Service
@DS("order") // 类级别注解:所有方法默认使用订单库
public class OrderService {
@Autowired
private OrderMapper orderMapper;
// 使用 order 数据源(继承类级别注解)
public Order getOrderById(Long orderId) {
return orderMapper.selectById(orderId);
}
// 使用 inventory 数据源(方法级别优先,覆盖类级别)
@DS("inventory")
public Inventory getInventory(Long productId) {
return inventoryMapper.selectById(productId);
}
// 使用 master 数据源(空字符串表示主数据源)
@DS("")
public void saveOrder(Order order) {
orderMapper.insert(order);
}
}
4.4 手动切换场景
在某些场景下,无法使用注解方式(如动态决定数据源名称),需要手动切换。
DynamicDataSourceContextHolder API
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
@Service
public class DynamicQueryService {
/**
* 手动切换数据源示例
*/
public Object queryFromDynamicSource(String dsName, Long id) {
try {
// 切换到指定数据源
DynamicDataSourceContextHolder.push(dsName);
// 执行查询操作
return someMapper.selectById(id);
} finally {
// 清除数据源,恢复默认
// 重要:必须在finally块中调用,确保清除
DynamicDataSourceContextHolder.poll();
}
}
/**
* 获取当前数据源名称
*/
public String getCurrentDataSource() {
return DynamicDataSourceContextHolder.peek();
}
}
多租户动态切换示例
@Service
public class TenantService {
@Autowired
private UserService userService;
/**
* 根据租户ID动态切换数据源
*/
public User getUserByTenant(Long tenantId, Long userId) {
String dsName = "tenant_" + tenantId;
try {
DynamicDataSourceContextHolder.push(dsName);
return userService.getById(userId);
} finally {
DynamicDataSourceContextHolder.poll();
}
}
}
适用场景
| 场景 | 推荐方式 |
|---|---|
| 数据源固定 | 注解方式 |
| 数据源动态(运行时决定) | 手动切换 |
| 需要嵌套切换 | 手动切换 |
| 多租户场景 | 手动切换或自定义注解处理器 |
SpEL表达式动态数据源
Dynamic-datasource支持在@DS注解中使用SpEL表达式,实现动态数据源计算。
基础示例:
@Service
public class TenantAwareService {
// 从方法参数获取租户ID:#tenantId 表示取参数值
@DS("#tenantId")
public Order getOrderByTenant(Long tenantId, Long orderId) {
return orderMapper.selectById(orderId);
}
// 分片计算:根据订单ID计算分片数据源
@DS("'shard_' + #orderId % 3")
public Order getOrderFromShard(Long orderId) {
return orderMapper.selectById(orderId);
}
// 按月分库:根据日期选择数据源
@DS("'archive_' + #date.year + '_' + #date.monthValue")
public List<Order> listArchiveOrders(LocalDate date) {
return archiveOrderMapper.selectByMonth(date);
}
}
| 表达式 | 说明 | 示例 |
|---|---|---|
#paramName |
方法参数引用 | @DS("#tenantId") |
#this |
当前对象引用 | @DS("#this.dataSource") |
T(Type).method() |
静态方法调用 | @DS("T(Math).abs(#id)") |
'prefix_' + #var |
字符串拼接 | @DS("'shard_' + #id % 3") |
#obj.property |
对象属性访问 | @DS("#order.tenantId") |
#obj.method() |
对象方法调用 | @DS("#context.getDs()") |
注意事项
- 必须在
finally块中清除数据源 - 避免在异步线程中忘记清除
- 手动切换与注解方式可以混用,但需注意优先级
- SpEL表达式计算结果必须与配置的数据源名称匹配(strict=true时会校验)
4.5 版本兼容性与升级指南
Dynamic-datasource框架经历了多个版本迭代,了解版本差异对于项目升级至关重要。
版本演进概览
| 版本 | 发布时间 | 主要变更 | 推荐使用 |
|---|---|---|---|
| 4.3.x | 2024年 | 支持Spring Boot 3.x,优化Seata集成 | ✅ 新项目推荐 |
| 4.2.x | 2023年 | 支持Spring Boot 2.7.x,增强动态数据源 | ✅ 稳定版本 |
| 3.6.x | 2022年 | 支持Spring Boot 2.3-2.6 | ⚠️ 维护阶段 |
| 3.5.x及以下 | 2021年及之前 | 基础版本 | ❌ 不推荐 |
Spring Boot版本适配
# Spring Boot 3.x 推荐配置
spring:
datasource:
dynamic:
primary: master
# Spring Boot 3.x 需使用4.3.0及以上版本
# Spring Boot 2.7.x 推荐配置
spring:
datasource:
dynamic:
primary: master
# 推荐使用4.2.0版本
3.x到4.x升级注意事项
// 3.x版本API(已废弃)
DynamicDataSourceContextHolder.setDataSource("order"); // 已废弃
DynamicDataSourceContextHolder.getDataSource(); // 已废弃
DynamicDataSourceContextHolder.clearDataSource(); // 已废弃
// 4.x版本API(推荐)
DynamicDataSourceContextHolder.push("order"); // 新API
DynamicDataSourceContextHolder.peek(); // 新API
DynamicDataSourceContextHolder.poll(); // 新API
升级迁移步骤
# 1. 更新Maven依赖版本
# 将dynamic-datasource-spring-boot-starter版本从3.x升级到4.x
# 2. 替换废弃API调用
# 搜索并替换setDataSource -> push
# 搜索并替换getDataSource -> peek
# 搜索并替换clearDataSource -> poll
# 3. 检查配置文件兼容性
# 4.x版本对某些配置属性名称有调整
常见升级问题及解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 启动报错"DataSource not found" | strict模式默认开启 | 检查数据源名称配置,或在开发环境设置strict=false |
| API调用编译错误 | 使用了废弃方法 | 替换为push/peek/poll新API |
| Seata集成失效 | Seata版本不匹配 | Seata需使用2.0.0及以上版本 |
| MyBatis-Plus分页异常 | 版本兼容问题 | MyBatis-Plus升级到3.5.x |
本节小结:基础实践部分涵盖了Maven依赖配置、YAML配置详解、@DS注解使用、手动切换API以及版本兼容性指南,为后续进阶实践奠定了基础。
五、进阶实践:高阶能力解锁
5.1 动态数据源管理
Dynamic-datasource支持在运行时动态添加和移除数据源,适用于多租户等场景。
动态添加数据源
@Service
public class DynamicDataSourceService {
@Autowired
private DynamicRoutingDataSource dynamicRoutingDataSource;
/**
* 动态添加数据源
* @param dsName 数据源名称
* @param url 数据库URL
* @param username 用户名
* @param password 密码
*/
public void addDataSource(String dsName, String url, String username, String password) {
// 创建数据源配置
HikariConfig config = new HikariConfig();
config.setDriverClassName("com.mysql.cj.jdbc.Driver");
config.setJdbcUrl(url);
config.setUsername(username);
config.setPassword(password);
config.setMinimumIdle(2);
config.setMaximumPoolSize(10);
config.setConnectionTimeout(30000);
config.setIdleTimeout(600000);
config.setMaxLifetime(1800000);
config.setConnectionTestQuery("SELECT 1");
// 创建数据源实例
HikariDataSource dataSource = new HikariDataSource(config);
// 添加到动态数据源管理器
dynamicRoutingDataSource.addDataSource(dsName, dataSource);
// 或者使用 DataSourceProperty 方式
// DataSourceProperty property = new DataSourceProperty();
// property.setUrl(url);
// property.setUsername(username);
// property.setPassword(password);
// property.setDriverClassName("com.mysql.cj.jdbc.Driver");
// dynamicRoutingDataSource.addDataSource(dsName, property);
}
}
动态移除数据源
@Service
public class DynamicDataSourceService {
@Autowired
private DynamicRoutingDataSource dynamicRoutingDataSource;
/**
* 动态移除数据源
* @param dsName 数据源名称
*/
public void removeDataSource(String dsName) {
// 检查数据源是否存在
if (!dynamicRoutingDataSource.getDataSources().containsKey(dsName)) {
throw new IllegalArgumentException("数据源 [" + dsName + "] 不存在");
}
// 移除数据源
DataSource removed = dynamicRoutingDataSource.removeDataSource(dsName);
// 关闭数据源连接池
if (removed instanceof HikariDataSource) {
((HikariDataSource) removed).close();
}
}
/**
* 获取所有数据源名称
*/
public Set<String> listDataSources() {
return dynamicRoutingDataSource.getDataSources().keySet();
}
}
多租户动态切换实战案例
/**
* 多租户数据源管理器
*/
@Service
@Slf4j
public class TenantDataSourceManager {
@Autowired
private DynamicRoutingDataSource dynamicRoutingDataSource;
@Autowired
private TenantConfigRepository tenantConfigRepository;
/**
* 初始化租户数据源(应用启动时调用)
*/
@PostConstruct
public void initTenantDataSources() {
List<TenantConfig> tenants = tenantConfigRepository.findAllActiveTenants();
for (TenantConfig tenant : tenants) {
try {
addTenantDataSource(tenant);
log.info("初始化租户数据源成功: {}", tenant.getTenantId());
} catch (Exception e) {
log.error("初始化租户数据源失败: {}", tenant.getTenantId(), e);
}
}
}
/**
* 添加租户数据源
*/
public void addTenantDataSource(TenantConfig tenant) {
String dsName = "tenant_" + tenant.getTenantId();
DataSourceProperty property = new DataSourceProperty();
property.setUrl(tenant.getDbUrl());
property.setUsername(tenant.getDbUsername());
property.setPassword(tenant.getDbPassword());
property.setDriverClassName("com.mysql.cj.jdbc.Driver");
// 配置连接池
HikariCpConfig hikari = new HikariCpConfig();
hikari.setMinimumIdle(2);
hikari.setMaximumPoolSize(10);
property.setHikari(hikari);
dynamicRoutingDataSource.addDataSource(dsName, property);
}
/**
* 移除租户数据源
*/
public void removeTenantDataSource(Long tenantId) {
String dsName = "tenant_" + tenantId;
if (dynamicRoutingDataSource.getDataSources().containsKey(dsName)) {
DataSource removed = dynamicRoutingDataSource.removeDataSource(dsName);
// 关闭连接池
if (removed instanceof AutoCloseable) {
try {
((AutoCloseable) removed).close();
} catch (Exception e) {
log.error("关闭数据源失败: {}", dsName, e);
}
}
}
}
/**
* 切换到指定租户数据源
*/
public void switchToTenant(Long tenantId) {
String dsName = "tenant_" + tenantId;
if (!dynamicRoutingDataSource.getDataSources().containsKey(dsName)) {
throw new IllegalArgumentException("租户数据源不存在: " + tenantId);
}
DynamicDataSourceContextHolder.push(dsName);
}
/**
* 清除租户数据源
*/
public void clearTenant() {
DynamicDataSourceContextHolder.poll();
}
}
/**
* 租户上下文工具类
*/
public class TenantContextHolder {
private static final ThreadLocal<Long> TENANT_ID = new ThreadLocal<>();
public static void setTenantId(Long tenantId) {
TENANT_ID.set(tenantId);
}
public static Long getTenantId() {
return TENANT_ID.get();
}
public static void clear() {
TENANT_ID.remove();
}
}
/**
* 租户切换切面
*/
@Aspect
@Component
public class TenantAspect {
@Autowired
private TenantDataSourceManager tenantDataSourceManager;
@Around("@annotation(tenant)")
public Object around(ProceedingJoinPoint point, Tenant tenant) throws Throwable {
Long tenantId = TenantContextHolder.getTenantId();
if (tenantId == null) {
throw new IllegalStateException("未设置租户ID");
}
try {
tenantDataSourceManager.switchToTenant(tenantId);
return point.proceed();
} finally {
tenantDataSourceManager.clearTenant();
}
}
}
// 使用示例
@RestController
@RequestMapping("/api")
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping("/orders/{orderId}")
public Order getOrder(@PathVariable Long orderId, @RequestHeader("X-Tenant-Id") Long tenantId) {
try {
TenantContextHolder.setTenantId(tenantId);
return orderService.getOrderById(orderId);
} finally {
TenantContextHolder.clear();
}
}
}
5.2 读写分离与负载均衡
主从配置示例
spring:
datasource:
dynamic:
primary: master
strict: false
datasource:
# 主库(写库)
master:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://master-db:3306/business_db
username: root
password: root
# 从库1(读库)
slave_1:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://slave-db-1:3306/business_db
username: root
password: root
# 从库2(读库)
slave_2:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://slave-db-2:3306/business_db
username: root
password: root
读写分离使用方式
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
/**
* 写操作 - 使用主库
*/
@DS("master")
public void createUser(User user) {
userMapper.insert(user);
}
/**
* 读操作 - 使用从库
* 可以指定具体的从库,也可以使用负载均衡
*/
@DS("slave_1")
public User getUserById(Long userId) {
return userMapper.selectById(userId);
}
/**
* 批量查询 - 使用另一个从库分担压力
*/
@DS("slave_2")
public List<User> listUsers() {
return userMapper.selectList(null);
}
}
使用@DSTransactional实现读写分离事务
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private InventoryMapper inventoryMapper;
/**
* 使用@DSTransactional注解
* 在事务开始时切换到主库,事务结束后恢复
*/
@DSTransactional
@DS("master") // 确保使用主库
public void createOrder(Order order) {
// 插入订单
orderMapper.insert(order);
// 扣减库存
inventoryMapper.decreaseStock(order.getProductId(), order.getQuantity());
}
}
5.3 多数据源事务处理
单数据源事务
单数据源事务使用Spring的@Transactional即可:
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
/**
* 单数据源事务
*/
@Transactional(rollbackFor = Exception.class)
@DS("order")
public void updateOrder(Order order) {
Order existingOrder = orderMapper.selectById(order.getId());
existingOrder.setStatus(order.getStatus());
orderMapper.updateById(existingOrder);
}
}
跨数据源事务的挑战
跨数据源事务存在以下问题:
- Spring的
@Transactional只能管理单个数据源的事务 - 多个数据源的事务无法统一提交/回滚
- 可能出现部分成功、部分失败的不一致状态
解决方案一:编程式事务管理
@Service
public class CrossDataSourceService {
@Autowired
private DataSourceTransactionManager masterTransactionManager;
@Autowired
private DataSourceTransactionManager orderTransactionManager;
@Autowired
private OrderMapper orderMapper;
@Autowired
private InventoryMapper inventoryMapper;
/**
* 编程式事务管理
* 每个数据源独立事务,需要手动处理一致性
*/
public void createOrderWithInventory(Order order) {
// 事务定义
TransactionDefinition definition = new DefaultTransactionDefinition();
// 开启订单库事务
TransactionStatus orderTx = orderTransactionManager.getTransaction(definition);
// 开启库存库事务
TransactionStatus inventoryTx = masterTransactionManager.getTransaction(definition);
try {
// 操作订单库
orderMapper.insert(order);
// 操作库存库
inventoryMapper.decreaseStock(order.getProductId(), order.getQuantity());
// 提交库存库事务
masterTransactionManager.commit(inventoryTx);
// 提交订单库事务
orderTransactionManager.commit(orderTx);
} catch (Exception e) {
// 回滚两个事务
orderTransactionManager.rollback(orderTx);
masterTransactionManager.rollback(inventoryTx);
throw new RuntimeException("创建订单失败", e);
}
}
}
解决方案二:Seata分布式事务
对于严格一致性的跨数据源事务,建议使用Seata:
<!-- 添加Seata依赖 -->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>4.3.0</version>
</dependency>
# Seata配置
seata:
enabled: true
application-id: business-service
tx-service-group: my_tx_group
service:
vgroup-mapping:
my_tx_group: default
registry:
type: nacos
nacos:
server-addr: localhost:8848
namespace: ""
group: SEATA_GROUP
# Dynamic-datasource配置
spring:
datasource:
dynamic:
seata: true # 开启Seata支持
seata-mode: AT # AT模式
primary: master
datasource:
master:
# ... 配置省略
order:
# ... 配置省略
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private InventoryMapper inventoryMapper;
/**
* 使用Seata分布式事务
* @GlobalTransactional 保证跨数据源事务一致性
*/
@GlobalTransactional(rollbackFor = Exception.class)
public void createOrder(Order order) {
// 操作订单库
orderMapper.insert(order);
// 操作库存库
inventoryMapper.decreaseStock(order.getProductId(), order.getQuantity());
// Seata自动管理跨数据源事务的提交/回滚
}
}
5.4 数据分片场景实践
Dynamic-datasource框架本身不提供分片功能,但可以与分库分表方案配合使用,或通过数据源分组实现简单的分片路由。
分片场景概述
| 分片类型 | 描述 | Dynamic-datasource适用性 |
|---|---|---|
| 分库 | 数据按规则分散到多个数据库 | ✅ 支持多数据源路由 |
| 分表 | 同库内数据分散到多个表 | ⚠️ 需配合MyBatis动态表名 |
| 分库+分表 | 数据分散到多库多表 | ⚠️ 建议使用ShardingSphere |
基于数据源的分库方案
# 按业务ID分库配置示例
spring:
datasource:
dynamic:
primary: master
datasource:
# 分片库配置
shard_0:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://shard-server-1:3306/shard_db_0
username: root
password: root
shard_1:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://shard-server-2:3306/shard_db_1
username: root
password: root
shard_2:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://shard-server-3:3306/shard_db_2
username: root
password: root
分片路由策略实现
/**
* 分片路由工具类
*/
public class ShardRouter {
// 分片数量
private static final int SHARD_COUNT = 3;
/**
* 根据业务ID计算分片索引
* @param businessId 业务ID
* @return 分片数据源名称
*/
public static String routeByBusinessId(Long businessId) {
int shardIndex = (int) (businessId % SHARD_COUNT);
return "shard_" + shardIndex;
}
/**
* 根据用户ID哈希分片
* @param userId 用户ID
* @return 分片数据源名称
*/
public static String routeByUserId(Long userId) {
int hash = Math.abs(userId.hashCode());
int shardIndex = hash % SHARD_COUNT;
return "shard_" + shardIndex;
}
/**
* 按时间范围分片(按月)
* @param date 日期
* @return 分片数据源名称
*/
public static String routeByMonth(LocalDate date) {
int monthValue = date.getMonthValue();
int shardIndex = monthValue % SHARD_COUNT;
return "shard_" + shardIndex;
}
}
/**
* 分片查询服务
*/
@Service
public class ShardOrderService {
@Autowired
private OrderMapper orderMapper;
/**
* 根据订单ID查询(自动路由到对应分片)
*/
public Order getOrderById(Long orderId) {
String shardDs = ShardRouter.routeByBusinessId(orderId);
try {
DynamicDataSourceContextHolder.push(shardDs);
return orderMapper.selectById(orderId);
} finally {
DynamicDataSourceContextHolder.poll();
}
}
/**
* 创建订单(路由到对应分片)
*/
public void createOrder(Order order) {
String shardDs = ShardRouter.routeByBusinessId(order.getId());
try {
DynamicDataSourceContextHolder.push(shardDs);
orderMapper.insert(order);
} finally {
DynamicDataSourceContextHolder.poll();
}
}
/**
* 按用户ID查询订单列表
*/
public List<Order> listOrdersByUserId(Long userId) {
String shardDs = ShardRouter.routeByUserId(userId);
try {
DynamicDataSourceContextHolder.push(shardDs);
LambdaQueryWrapper<Order> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Order::getUserId, userId);
return orderMapper.selectList(wrapper);
} finally {
DynamicDataSourceContextHolder.poll();
}
}
}
动态表名分表方案
/**
* 动态表名处理器
* 配合MyBatis-Plus实现同库分表
*/
@Component
public class DynamicTableNameHandler {
/**
* 获取分表名称
* @param baseTableName 基础表名
* @param shardKey 分片键
* @return 实际表名
*/
public static String getShardTableName(String baseTableName, Long shardKey) {
int tableIndex = (int) (shardKey % 10); // 10张分表
return baseTableName + "_" + tableIndex;
}
/**
* 获取按月的分表名称
* @param baseTableName 庺础表名
* @param date 日期
* @return 实际表名
*/
public static String getMonthTableName(String baseTableName, LocalDate date) {
String monthStr = date.format(DateTimeFormatter.ofPattern("yyyyMM"));
return baseTableName + "_" + monthStr;
}
}
/**
* 动态表名Mapper示例
*/
public interface OrderLogMapper extends BaseMapper<OrderLog> {
/**
* 动态表名查询
* @param tableName 实际表名
* @param wrapper 查询条件
* @return 结果列表
*/
@Select("SELECT * FROM ${tableName} WHERE user_id = #{userId}")
List<OrderLog> selectByDynamicTable(@Param("tableName") String tableName,
@Param("userId") Long userId);
}
/**
* 分表查询服务
*/
@Service
public class OrderLogService {
@Autowired
private OrderLogMapper orderLogMapper;
/**
* 查询订单日志(分库+分表)
*/
public List<OrderLog> getOrderLogs(Long orderId, LocalDate date) {
// 1. 确定分库
String shardDs = ShardRouter.routeByBusinessId(orderId);
// 2. 确定分表
String tableName = DynamicTableNameHandler.getMonthTableName("order_log", date);
try {
DynamicDataSourceContextHolder.push(shardDs);
return orderLogMapper.selectByDynamicTable(tableName, orderId);
} finally {
DynamicDataSourceContextHolder.poll();
}
}
}
分片场景注意事项
// ❌ 错误:跨分片查询不使用聚合
@Service
public class WrongShardService {
/**
* 错误示例:直接查询所有分片
* 会导致数据不完整或路由错误
*/
public List<Order> listAllOrders() {
// 无法在一个数据源上查询所有分片数据
return orderMapper.selectList(null); // 只能查到一个分片
}
}
// ✅ 正确:跨分片查询使用聚合服务
@Service
public class CorrectShardService {
/**
* 正确示例:遍历所有分片聚合查询
*/
public List<Order> listAllOrders() {
List<Order> allOrders = new ArrayList<>();
// 遍历所有分片
for (int i = 0; i < 3; i++) {
String shardDs = "shard_" + i;
try {
DynamicDataSourceContextHolder.push(shardDs);
List<Order> shardOrders = orderMapper.selectList(null);
allOrders.addAll(shardOrders);
} finally {
DynamicDataSourceContextHolder.poll();
}
}
return allOrders;
}
}
5.5 跨数据源查询聚合
在多数据源场景下,经常需要从不同数据源查询数据并进行聚合处理,这是进阶使用的重要能力。
跨数据源聚合场景
| 场景 | 描述 | 挑战 |
|---|---|---|
| 用户-订单聚合 | 用户库+订单库联合查询 | 数据分散,无法JOIN |
| 商品-库存聚合 | 商品库+库存库联合查询 | 需要内存拼接 |
| 报表统计 | 多业务库数据汇总 | 性能优化、分页处理 |
| 数据迁移校验 | 新旧库数据比对 | 数据一致性验证 |
基础聚合模式
/**
* 跨数据源聚合服务
*/
@Service
public class UserOrderAggregateService {
@Autowired
private UserMapper userMapper;
@Autowired
private OrderMapper orderMapper;
/**
* 基础聚合:用户+订单信息组合
*/
public UserOrderDTO getUserWithOrders(Long userId) {
// 1. 查询用户信息(用户库)
User user;
try {
DynamicDataSourceContextHolder.push("user");
user = userMapper.selectById(userId);
} finally {
DynamicDataSourceContextHolder.poll();
}
if (user == null) {
return null;
}
// 2. 查询订单信息(订单库)
List<Order> orders;
try {
DynamicDataSourceContextHolder.push("order");
LambdaQueryWrapper<Order> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Order::getUserId, userId);
orders = orderMapper.selectList(wrapper);
} finally {
DynamicDataSourceContextHolder.poll();
}
// 3. 内存聚合
UserOrderDTO dto = new UserOrderDTO();
dto.setUser(user);
dto.setOrders(orders);
dto.setOrderCount(orders.size());
dto.setTotalAmount(orders.stream()
.map(Order::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add));
return dto;
}
}
并行查询优化
/**
* 并行聚合服务(提升查询性能)
*/
@Service
public class ParallelAggregateService {
@Autowired
private UserMapper userMapper;
@Autowired
private OrderMapper orderMapper;
@Autowired
private InventoryMapper inventoryMapper;
/**
* 并行查询多数据源,提升性能
*/
public CompletableFuture<OrderDetailDTO> getOrderDetailAsync(Long orderId) {
// 异步查询订单
CompletableFuture<Order> orderFuture = CompletableFuture.supplyAsync(() -> {
try {
DynamicDataSourceContextHolder.push("order");
return orderMapper.selectById(orderId);
} finally {
DynamicDataSourceContextHolder.poll();
}
});
// 异步查询商品信息
CompletableFuture<Product> productFuture = orderFuture.thenApplyAsync(order -> {
if (order == null) return null;
try {
DynamicDataSourceContextHolder.push("product");
return productMapper.selectById(order.getProductId());
} finally {
DynamicDataSourceContextHolder.poll();
}
});
// 异步查询库存信息
CompletableFuture<Inventory> inventoryFuture = orderFuture.thenApplyAsync(order -> {
if (order == null) return null;
try {
DynamicDataSourceContextHolder.push("inventory");
return inventoryMapper.selectByProductId(order.getProductId());
} finally {
DynamicDataSourceContextHolder.poll();
}
});
// 异步查询用户信息
CompletableFuture<User> userFuture = orderFuture.thenApplyAsync(order -> {
if (order == null) return null;
try {
DynamicDataSourceContextHolder.push("user");
return userMapper.selectById(order.getUserId());
} finally {
DynamicDataSourceContextHolder.poll();
}
});
// 等待所有查询完成,聚合结果
return CompletableFuture.allOf(productFuture, inventoryFuture, userFuture)
.thenApply(v -> {
Order order = orderFuture.join();
if (order == null) return null;
OrderDetailDTO dto = new OrderDetailDTO();
dto.setOrder(order);
dto.setProduct(productFuture.join());
dto.setInventory(inventoryFuture.join());
dto.setUser(userFuture.join());
return dto;
});
}
/**
* 同步方式调用并行聚合
*/
public OrderDetailDTO getOrderDetail(Long orderId) {
try {
return getOrderDetailAsync(orderId).get(10, TimeUnit.SECONDS);
} catch (TimeoutException e) {
throw new RuntimeException("查询超时");
} catch (Exception e) {
throw new RuntimeException("查询失败", e);
}
}
}
分页聚合处理
/**
* 跨数据源分页聚合
*/
@Service
public class PaginatedAggregateService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private UserMapper userMapper;
/**
* 跨库分页查询(订单+用户信息)
* 分页在订单库,用户信息补充查询
*/
public IPage<OrderUserDTO> pageOrdersWithUser(int pageNum, int pageSize) {
// 1. 从订单库分页查询订单
Page<Order> orderPage;
try {
DynamicDataSourceContextHolder.push("order");
orderPage = orderMapper.selectPage(new Page<>(pageNum, pageSize), null);
} finally {
DynamicDataSourceContextHolder.poll();
}
// 2. 收集所有用户ID
Set<Long> userIds = orderPage.getRecords().stream()
.map(Order::getUserId)
.collect(Collectors.toSet());
// 3. 批量查询用户信息
Map<Long, User> userMap;
try {
DynamicDataSourceContextHolder.push("user");
List<User> users = userMapper.selectBatchIds(userIds);
userMap = users.stream()
.collect(Collectors.toMap(User::getId, Function.identity()));
} finally {
DynamicDataSourceContextHolder.poll();
}
// 4. 组装分页结果
List<OrderUserDTO> dtoList = orderPage.getRecords().stream()
.map(order -> {
OrderUserDTO dto = new OrderUserDTO();
dto.setOrder(order);
dto.setUser(userMap.get(order.getUserId()));
return dto;
})
.collect(Collectors.toList());
// 5. 构造分页返回对象
Page<OrderUserDTO> resultPage = new Page<>(pageNum, pageSize);
resultPage.setRecords(dtoList);
resultPage.setTotal(orderPage.getTotal());
resultPage.setPages(orderPage.getPages());
return resultPage;
}
}
报表统计聚合
/**
* 跨库报表统计服务
*/
@Service
public class ReportAggregateService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private PaymentMapper paymentMapper;
@Autowired
private InventoryMapper inventoryMapper;
/**
* 多库数据汇总报表
*/
public BusinessReportDTO generateDailyReport(LocalDate date) {
BusinessReportDTO report = new BusinessReportDTO();
report.setDate(date);
// 1. 订单统计(订单库)
try {
DynamicDataSourceContextHolder.push("order");
// 日订单总数
LambdaQueryWrapper<Order> wrapper = new LambdaQueryWrapper<>();
wrapper.between(Order::getCreateTime,
date.atStartOfDay(), date.plusDays(1).atStartOfDay());
Long orderCount = orderMapper.selectCount(wrapper);
report.setOrderCount(orderCount);
// 日销售额
List<Order> orders = orderMapper.selectList(wrapper);
BigDecimal totalAmount = orders.stream()
.map(Order::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
report.setTotalAmount(totalAmount);
} finally {
DynamicDataSourceContextHolder.poll();
}
// 2. 支付统计(支付库)
try {
DynamicDataSourceContextHolder.push("payment");
LambdaQueryWrapper<Payment> wrapper = new LambdaQueryWrapper<>();
wrapper.between(Payment::getCreateTime,
date.atStartOfDay(), date.plusDays(1).atStartOfDay());
Long paymentCount = paymentMapper.selectCount(wrapper);
report.setPaymentCount(paymentCount);
// 支付金额
List<Payment> payments = paymentMapper.selectList(wrapper);
BigDecimal totalPayment = payments.stream()
.filter(p -> "SUCCESS".equals(p.getStatus()))
.map(Payment::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
report.setTotalPayment(totalPayment);
} finally {
DynamicDataSourceContextHolder.poll();
}
// 3. 库存统计(库存库)
try {
DynamicDataSourceContextHolder.push("inventory");
// 库存总量
Long totalStock = inventoryMapper.sumTotalStock();
report.setTotalStock(totalStock);
// 低库存商品数
Long lowStockCount = inventoryMapper.countLowStock(10);
report.setLowStockCount(lowStockCount);
} finally {
DynamicDataSourceContextHolder.poll();
}
// 4. 计算衍生指标
report.setPaymentRate(report.getOrderCount() > 0
? (double) report.getPaymentCount() / report.getOrderCount()
: 0);
report.setAvgOrderAmount(report.getOrderCount() > 0
? report.getTotalAmount().divide(new BigDecimal(report.getOrderCount()), 2, RoundingMode.HALF_UP)
: BigDecimal.ZERO);
return report;
}
}
数据一致性校验
/**
* 跨库数据校验服务
* 用于数据迁移、同步场景
*/
@Service
public class DataVerifyService {
@Autowired
private OldOrderMapper oldOrderMapper; // 旧库
@Autowired
private NewOrderMapper newOrderMapper; // 新库
/**
* 数据迁移校验
*/
public DataVerifyResult verifyMigration(LocalDate migrateDate) {
DataVerifyResult result = new DataVerifyResult();
// 1. 从旧库查询迁移范围内的数据总数
Long oldCount;
List<Order> oldOrders;
try {
DynamicDataSourceContextHolder.push("old_system");
LambdaQueryWrapper<Order> wrapper = new LambdaQueryWrapper<>();
wrapper.between(Order::getCreateTime,
migrateDate.atStartOfDay(), migrateDate.plusDays(1).atStartOfDay());
oldCount = oldOrderMapper.selectCount(wrapper);
oldOrders = oldOrderMapper.selectList(wrapper);
} finally {
DynamicDataSourceContextHolder.poll();
}
// 2. 从新库查询已迁移数据总数
Long newCount;
List<Order> newOrders;
try {
DynamicDataSourceContextHolder.push("new_system");
LambdaQueryWrapper<Order> wrapper = new LambdaQueryWrapper<>();
wrapper.between(Order::getCreateTime,
migrateDate.atStartOfDay(), migrateDate.plusDays(1).atStartOfDay());
newCount = newOrderMapper.selectCount(wrapper);
newOrders = newOrderMapper.selectList(wrapper);
} finally {
DynamicDataSourceContextHolder.poll();
}
// 3. 数量对比
result.setOldCount(oldCount);
result.setNewCount(newCount);
result.setCountMatch(oldCount.equals(newCount));
// 4. 数据内容对比
Map<Long, Order> oldMap = oldOrders.stream()
.collect(Collectors.toMap(Order::getId, Function.identity()));
Map<Long, Order> newMap = newOrders.stream()
.collect(Collectors.toMap(Order::getId, Function.identity()));
List<DataDiff> diffs = new ArrayList<>();
for (Order oldOrder : oldOrders) {
Order newOrder = newMap.get(oldOrder.getId());
if (newOrder == null) {
diffs.add(new DataDiff(oldOrder.getId(), "MISSING", "新库不存在"));
} else {
// 对比关键字段
if (!Objects.equals(oldOrder.getAmount(), newOrder.getAmount())) {
diffs.add(new DataDiff(oldOrder.getId(), "AMOUNT_DIFF",
"旧:" + oldOrder.getAmount() + ", 新:" + newOrder.getAmount()));
}
if (!Objects.equals(oldOrder.getStatus(), newOrder.getStatus())) {
diffs.add(new DataDiff(oldOrder.getId(), "STATUS_DIFF",
"旧:" + oldOrder.getStatus() + ", 新:" + newOrder.getStatus()));
}
}
}
result.setDiffs(diffs);
result.setDataMatch(diffs.isEmpty());
return result;
}
}
聚合查询性能优化建议
| 优化策略 | 描述 | 适用场景 |
|---|---|---|
| 并行查询 | CompletableFuture异步并行 | 多数据源独立查询 |
| 批量查询 | 批量ID查询替代逐条查询 | 关联数据补全 |
| 缓存辅助 | 热点数据Redis缓存 | 用户信息等高频查询 |
| 分页优先 | 分页在主库,关联数据补充 | 大数据量展示 |
| 预聚合 | 定时任务预计算聚合结果 | 报表统计场景 |
5.6 MyBatis-Plus深度整合
配置整合
@Configuration
@MapperScan("com.example.mapper")
public class MybatisPlusConfig {
/**
* 分页插件
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
// 防全表更新/删除插件
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
// 乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}
多数据源分页示例
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private InventoryMapper inventoryMapper;
/**
* 订单库分页查询
*/
@DS("order")
public IPage<Order> pageOrders(int pageNum, int pageSize) {
Page<Order> page = new Page<>(pageNum, pageSize);
return orderMapper.selectPage(page, null);
}
/**
* 库存库分页查询
*/
@DS("master")
public IPage<Inventory> pageInventories(int pageNum, int pageSize) {
Page<Inventory> page = new Page<>(pageNum, pageSize);
return inventoryMapper.selectPage(page, null);
}
}
乐观锁使用注意事项
// 实体类
@Data
@TableName("t_product")
public class Product {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String name;
private Integer stock;
@Version // 乐观锁版本号字段
private Integer version;
}
// Service
@Service
public class ProductService {
@Autowired
private ProductMapper productMapper;
/**
* 扣减库存(使用乐观锁)
* 注意:乐观锁需要在同一个数据源中操作
*/
@DS("master")
@Transactional(rollbackFor = Exception.class)
public boolean decreaseStock(Long productId, Integer quantity) {
Product product = productMapper.selectById(productId);
if (product.getStock() < quantity) {
throw new RuntimeException("库存不足");
}
product.setStock(product.getStock() - quantity);
// MyBatis-Plus会自动处理版本号
return productMapper.updateById(product) > 0;
}
}
六、避坑指南:常见问题与解决方案
6.1 数据源切换不生效
问题描述
使用@DS注解后,数据源切换未生效,SQL仍在默认数据源执行。
问题根因分析
| 原因 | 描述 |
|---|---|
| 注解位置错误 | @DS注解在private方法上无效 |
| AOP代理问题 | 同类方法调用,绕过代理 |
| 数据源名称错误 | 配置文件中不存在该数据源 |
| strict模式 | strict=false时,不存在的数据源会回退到默认 |
排查步骤
// 1. 检查当前数据源
String currentDs = DynamicDataSourceContextHolder.peek();
log.info("当前数据源: {}", currentDs);
// 2. 检查已配置的数据源
@Autowired
private DynamicRoutingDataSource dynamicRoutingDataSource;
Set<String> dsNames = dynamicRoutingDataSource.getDataSources().keySet();
log.info("已配置数据源: {}", dsNames);
解决方案
// ❌ 错误:同类方法调用,绕过代理
@Service
public class OrderService {
public void methodA() {
this.methodB(); // 直接调用,@DS不生效
}
@DS("order")
public void methodB() {
// ...
}
}
// ✅ 正确:注入自身代理
@Service
public class OrderService {
@Autowired
@Lazy // 避免循环依赖
private OrderService self;
public void methodA() {
self.methodB(); // 通过代理调用,@DS生效
}
@DS("order")
public void methodB() {
// ...
}
}
// ✅ 正确:使用AopContext
@Service
public class OrderService {
public void methodA() {
OrderService proxy = (OrderService) AopContext.currentProxy();
proxy.methodB();
}
@DS("order")
public void methodB() {
// ...
}
}
6.2 事务失效问题
问题描述
@DS与@Transactional同时使用时,数据源切换失效或事务不生效。
问题场景还原
// ❌ 错误示例:事务导致数据源切换失效
@Service
public class OrderService {
@Transactional(rollbackFor = Exception.class)
@DS("order")
public void createOrder(Order order) {
// 问题:@Transactional先于@DS执行
// 事务管理器可能在默认数据源开启事务
// 导致后续的@DS切换不生效
orderMapper.insert(order);
}
}
根因分析
Spring的事务管理器在事务开始时获取Connection,此时数据源尚未切换,导致事务绑定到错误的数据源。
正确使用姿势
// ✅ 方案一:确保注解顺序正确(@DS在@Transaction之前)
@Service
public class OrderService {
@DS("order") // 先切换数据源
@Transactional(rollbackFor = Exception.class) // 再开启事务
public void createOrder(Order order) {
orderMapper.insert(order);
}
}
// ✅ 方案二:使用@DSTransactional(框架提供的组合注解)
@Service
public class OrderService {
@DSTransactional // 自动处理数据源切换和事务
@DS("order")
public void createOrder(Order order) {
orderMapper.insert(order);
}
}
// ✅ 方案三:类级别注解 + 方法级事务
@Service
@DS("order") // 类级别指定数据源
public class OrderService {
@Transactional(rollbackFor = Exception.class)
public void createOrder(Order order) {
orderMapper.insert(order);
}
}
6.3 连接池配置陷阱
问题描述
HikariCP配置参数不生效或产生警告。
不支持的配置
# ❌ 错误:部分HikariCP属性在dynamic-datasource中不支持
spring:
datasource:
dynamic:
datasource:
master:
hikari:
pool-name: MyPool # 不支持,会产生警告
auto-commit: true # 不支持
read-only: false # 不支持
支持的配置清单
# ✅ 正确:使用支持的配置参数
spring:
datasource:
dynamic:
datasource:
master:
hikari:
# 连接池大小配置
minimum-idle: 5 # 最小空闲连接数
maximum-pool-size: 20 # 最大连接数
# 超时配置
connection-timeout: 30000 # 连接超时(毫秒)
idle-timeout: 600000 # 空闲超时(毫秒)
max-lifetime: 1800000 # 最大存活时间(毫秒)
# 连接验证
connection-test-query: SELECT 1 # 连接测试查询
# 泄漏检测
leak-detection-threshold: 60000 # 泄漏检测阈值(毫秒)
连接泄漏检测
// 开启连接泄漏检测
spring:
datasource:
dynamic:
datasource:
master:
hikari:
leak-detection-threshold: 30000 # 30秒未归还连接视为泄漏
// 日志输出示例
// WARNING - Connection leak detection triggered for connection...
6.4 ThreadLocal线程安全
问题描述
在异步线程或线程池中,数据源切换异常或污染其他线程。
问题场景还原
// ❌ 错误:异步线程未清理数据源
@Service
public class AsyncService {
@DS("order")
public void processOrder(Order order) {
// 在异步线程中执行
CompletableFuture.runAsync(() -> {
// 问题:ThreadLocal不会传递到异步线程
// 数据源切换不生效
orderMapper.updateById(order);
});
}
}
// ❌ 错误:线程池复用导致数据源污染
@Service
public class ThreadPoolService {
@Autowired
private ExecutorService executor;
@DS("order")
public void task1() {
executor.submit(() -> {
// 使用order数据源
// 如果未清理,下一个任务可能错误使用order数据源
});
}
public void task2() {
// 可能错误地使用了task1遗留的数据源
executor.submit(() -> {
// ...
});
}
}
正确使用模式
// ✅ 正确:手动传递数据源上下文
@Service
public class AsyncService {
@DS("order")
public void processOrder(Order order) {
// 获取当前数据源
String currentDs = DynamicDataSourceContextHolder.peek();
CompletableFuture.runAsync(() -> {
try {
// 在异步线程中设置数据源
DynamicDataSourceContextHolder.push(currentDs);
orderMapper.updateById(order);
} finally {
// 必须清理
DynamicDataSourceContextHolder.poll();
}
});
}
}
// ✅ 正确:使用装饰线程池
public class DataSourceAwareTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
// 捕获当前线程的数据源
String dsName = DynamicDataSourceContextHolder.peek();
return () -> {
try {
// 设置到新线程
if (dsName != null) {
DynamicDataSourceContextHolder.push(dsName);
}
runnable.run();
} finally {
// 清理
DynamicDataSourceContextHolder.poll();
}
};
}
}
// 配置线程池
@Configuration
public class AsyncConfig {
@Bean
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
// 设置任务装饰器
executor.setTaskDecorator(new DataSourceAwareTaskDecorator());
return executor;
}
}
6.5 性能优化要点
性能基准参考
以下为Dynamic-datasource框架在不同场景下的性能参考数据(基于4.3.0版本测试):
| 测试场景 | 测试条件 | 结果数据 |
|---|---|---|
| 数据源切换开销 | 单线程1000次切换 | 平均耗时 <0.1ms/次 |
| @DS注解解析 | AOP拦截10000次调用 | 平均耗时 <0.05ms/次 |
| 动态添加数据源 | 添加1个HikariCP数据源 | 初始化耗时 50-200ms |
| 并发切换压力 | 100线程并发切换 | 吞吐量 >50000次/秒 |
| 连接获取开销 | HikariCP连接池 | 连接获取 <1ms |
性能影响因素
// 影响性能的关键因素及优化建议:
// 1. 数据源数量
// 建议:控制在10个以内,过多会增加路由决策开销
// 大量数据源场景考虑使用分组或动态加载
// 2. 连接池配置
// 禁忌:max-pool-size设置过大,导致资源浪费
// 建议:根据实际并发量设置,公式:(核心数*2) + 有效磁盘数
// 3. 事务边界
// 禁忌:事务范围过大,长时间占用连接
// 建议:事务最小化,避免在事务内执行远程调用、文件操作
// 4. SpEL表达式复杂度
// 禁忌:SpEL表达式过于复杂,每次调用都需要解析
// 建议:简单表达式优先,复杂逻辑可预计算后手动切换
连接池调优策略
spring:
datasource:
dynamic:
datasource:
master:
hikari:
# 根据业务并发量调整
# 公式:maximum-pool-size = (核心数 * 2) + 有效磁盘数
maximum-pool-size: 20
# 最小空闲连接数,建议与maximum-pool-size相近或为其1/2
minimum-idle: 10
# 连接超时时间,不宜过长
connection-timeout: 30000
# 空闲连接超时,建议10分钟
idle-timeout: 600000
# 连接最大存活时间,建议30分钟
max-lifetime: 1800000
数据源预热
@Component
@Slf4j
public class DataSourceWarmUp implements ApplicationRunner {
@Autowired
private DynamicRoutingDataSource dynamicRoutingDataSource;
@Override
public void run(ApplicationArguments args) {
log.info("开始预热数据源连接池...");
for (Map.Entry<String, DataSource> entry : dynamicRoutingDataSource.getDataSources().entrySet()) {
String dsName = entry.getKey();
DataSource ds = entry.getValue();
if (ds instanceof HikariDataSource) {
HikariDataSource hikari = (HikariDataSource) ds;
// 获取最小连接数,触发连接池初始化
int minIdle = hikari.getHikariConfigMXBean().getMinimumIdle();
log.info("预热数据源 [{}],初始化 {} 个连接", dsName, minIdle);
}
}
log.info("数据源预热完成");
}
}
慢SQL监控配置
# MyBatis-Plus慢SQL监控
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
# 慢SQL阈值(毫秒)
slow-query-threshold: 3000
// 自定义慢SQL拦截器
@Intercepts({
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
@Component
@Slf4j
public class SlowSqlInterceptor implements Interceptor {
private static final long SLOW_SQL_THRESHOLD = 3000; // 3秒
@Override
public Object intercept(Invocation invocation) throws Throwable {
long startTime = System.currentTimeMillis();
try {
return invocation.proceed();
} finally {
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
if (duration > SLOW_SQL_THRESHOLD) {
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
log.warn("====== 慢SQL警告 ======");
log.warn("SQL ID: {}", ms.getId());
log.warn("执行时间: {} ms (阈值: {} ms)", duration, SLOW_SQL_THRESHOLD);
log.warn("======================");
}
}
}
}
七、最佳实践:工程化建议
7.1 使用规范
数据源命名规范
# 推荐的命名规范
spring:
datasource:
dynamic:
primary: master # 主数据源统一命名为master
datasource:
# 按业务域命名
order: # 订单域
inventory: # 库存域
user: # 用户域
report: # 报表域
# 或按读写分离命名
master: # 主库(写)
slave_1: # 从库1(读)
slave_2: # 从库2(读)
Service层设计规范
// ✅ 推荐:按数据源拆分Service
@Service
@DS("order")
public class OrderQueryService {
// 只包含订单库的查询操作
}
@Service
@DS("order")
public class OrderCommandService {
// 只包含订单库的写入操作
}
// ❌ 不推荐:一个Service跨多个数据源
@Service
public class MixedService {
@DS("order")
public void orderOperation() {}
@DS("inventory")
public void inventoryOperation() {}
}
事务边界设计原则
// 原则1:事务最小化
// ✅ 正确:事务只包含必要的操作
@DS("order")
@Transactional(rollbackFor = Exception.class)
public void createOrder(Order order) {
orderMapper.insert(order);
// 不包含远程调用、文件操作等非数据库操作
}
// ❌ 错误:事务包含过多操作
@DS("order")
@Transactional(rollbackFor = Exception.class)
public void createOrderWithNotify(Order order) {
orderMapper.insert(order);
// 远程调用不应在事务内
notificationService.sendEmail(order); // 可能导致事务长时间占用
}
// 原则2:明确事务传播行为
@DS("order")
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public void createOrder(Order order) {
orderMapper.insert(order);
}
// 原则3:只读事务优化查询性能
@DS("order")
@Transactional(readOnly = true)
public List<Order> listOrders() {
return orderMapper.selectList(null);
}
7.2 监控与运维
数据源健康检查
@RestController
@RequestMapping("/actuator")
public class DataSourceHealthController {
@Autowired
private DynamicRoutingDataSource dynamicRoutingDataSource;
@GetMapping("/datasource-health")
public Map<String, Object> dataSourceHealth() {
Map<String, Object> result = new HashMap<>();
for (Map.Entry<String, DataSource> entry : dynamicRoutingDataSource.getDataSources().entrySet()) {
String dsName = entry.getKey();
DataSource ds = entry.getValue();
Map<String, Object> dsInfo = new HashMap<>();
if (ds instanceof HikariDataSource) {
HikariDataSource hikari = (HikariDataSource) ds;
HikariPoolMXBean pool = hikari.getHikariPoolMXBean();
dsInfo.put("activeConnections", pool.getActiveConnections());
dsInfo.put("idleConnections", pool.getIdleConnections());
dsInfo.put("totalConnections", pool.getTotalConnections());
dsInfo.put("threadsAwaitingConnection", pool.getThreadsAwaitingConnection());
// 健康状态判断
boolean healthy = pool.getIdleConnections() > 0
|| pool.getActiveConnections() < hikari.getMaximumPoolSize();
dsInfo.put("healthy", healthy);
}
result.put(dsName, dsInfo);
}
return result;
}
}
连接池监控指标
@Component
@Slf4j
public class DataSourceMonitor {
@Autowired
private DynamicRoutingDataSource dynamicRoutingDataSource;
@Scheduled(fixedRate = 60000) // 每分钟执行
public void monitorConnectionPool() {
for (Map.Entry<String, DataSource> entry : dynamicRoutingDataSource.getDataSources().entrySet()) {
String dsName = entry.getKey();
DataSource ds = entry.getValue();
if (ds instanceof HikariDataSource) {
HikariDataSource hikari = (HikariDataSource) ds;
HikariPoolMXBean pool = hikari.getHikariPoolMXBean();
log.info("数据源 [{}] 连接池状态: 活跃={}, 空闲={}, 总数={}, 等待={}",
dsName,
pool.getActiveConnections(),
pool.getIdleConnections(),
pool.getTotalConnections(),
pool.getThreadsAwaitingConnection());
// 告警判断
double usageRate = (double) pool.getActiveConnections() / hikari.getMaximumPoolSize();
if (usageRate > 0.8) {
log.warn("数据源 [{}] 连接池使用率过高: {}%", dsName, String.format("%.2f", usageRate * 100));
}
}
}
}
}
日志与告警策略
# Logback配置示例
logging:
level:
com.baomidou.dynamic.datasource: DEBUG
com.zaxxer.hikari: INFO
// 关键操作日志
@Aspect
@Component
@Slf4j
public class DataSourceLogAspect {
@Around("@annotation(ds)")
public Object logDataSourceSwitch(ProceedingJoinPoint point, DS ds) throws Throwable {
String dsName = ds.value();
String methodName = point.getSignature().getName();
log.debug("方法 [{}] 切换到数据源 [{}]", methodName, dsName);
long startTime = System.currentTimeMillis();
try {
Object result = point.proceed();
long duration = System.currentTimeMillis() - startTime;
log.debug("方法 [{}] 执行完成,耗时 {} ms", methodName, duration);
return result;
} catch (Exception e) {
log.error("方法 [{}] 执行异常: {}", methodName, e.getMessage());
throw e;
}
}
}
7.3 架构演进建议
多数据源 vs 微服务拆分选择
| 场景 | 推荐方案 |
|---|---|
| 业务耦合度高,但需要访问不同库 | 多数据源 |
| 业务相对独立,团队规模小 | 多数据源 |
| 业务独立演进,团队规模大 | 微服务拆分 |
| 需要独立部署、独立扩展 | 微服务拆分 |
| 跨数据源事务要求严格 | 多数据源 + Seata |
长期演进路线
阶段一:单体应用 + 多数据源
└── 快速迭代,业务验证
阶段二:模块化单体 + 多数据源
└── 代码层面解耦,为拆分做准备
阶段三:微服务拆分
└── 按业务域拆分,逐步迁移
└── 多数据源 -> 每个服务独立数据源
阶段四:服务治理
└── 服务间通信(Feign/Dubbo)
└── 分布式事务(Seata/Saga)
└── 链路追踪(SkyWalking/Zipkin)
八、总结
核心要点回顾
-
场景认知:多数据源框架适用于读写分离、多租户、系统整合等场景,但需权衡是否应选择微服务架构。
-
方案选型:Dynamic-datasource适合中小规模多数据源场景,轻量级、易集成;ShardingSphere适合大规模分库分表场景。
-
核心原理:理解ThreadLocal上下文隔离、AOP切面、动态路由三大机制,是正确使用框架的基础。
-
使用规范:遵循注解优先级、事务边界、命名规范等最佳实践,避免踩坑。
-
避坑指南:关注事务失效、线程安全、连接池配置等常见问题,做好监控和告警。
框架适用边界
| 适用场景 | 不适用场景 |
|---|---|
| 3-5个数据源以内 | 超大规模分库分表(建议ShardingSphere) |
| 业务间有一定耦合 | 业务完全独立,需要独立部署 |
| 跨数据源事务要求不高 | 强一致性跨库事务(需配合Seata) |
| 团队规模较小 | 多团队独立开发部署 |
学习资源推荐
本文从问题场景出发,系统介绍了Dynamic-datasource框架的核心原理与实践方法。

浙公网安备 33010602011771号