编辑 | blame | 历史 | 原始文档

多数据源切换问题修复说明

问题描述

在同一个 Service 方法中调用多个不同数据源的 Mapper 时,出现**数据源切换失效**的问题:

@Service
public class DepartmentSyncServiceImpl {
    @Autowired
    private DepartmentSyncMapper departmentSyncMapper; // @DataSource(SQLSERVER)
    
    @Autowired
    private SysDeptMapper sysDeptMapper; // 默认 MySQL
    
    @Transactional
    public AjaxResult syncBranchDepartments() {
        // 第一步:从 SQL Server 查询数据
        List<DepartmentSyncDTO> data = departmentSyncMapper.selectBranchDepartments();
        // ✅ 这里能正确查询 SQL Server
        
        // 第二步:写入 MySQL
        sysDeptMapper.insertDept(dept);
        // ❌ 问题:这里也去了 MySQL,因为数据源被清除了
    }
}

现象
- 第一个 Mapper 调用能够正确切换到 SQL Server
- 调用完成后,数据源被清除
- 后续的 MySQL Mapper 调用正常
- 但如果再次调用 SQL Server Mapper,会失败(使用了 MySQL 数据源)


问题根因分析

原始的 DataSourceAspect 实现

@Around("dsPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
    DataSource dataSource = getDataSource(point);
    
    if (StringUtils.isNotNull(dataSource)) {
        // 设置数据源
        DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name());
    }
    
    try {
        return point.proceed();
    }
    finally {
        // ❌ 问题:无论如何都会清除数据源
        DynamicDataSourceContextHolder.clearDataSourceType();
    }
}

问题点

  1. 无条件清除数据源
  • 每次 Mapper 方法执行完成后,在 finally 块中都会调用 clearDataSourceType()
  • 导致数据源上下文被清空,回到默认数据源(MySQL)
  1. 不支持嵌套调用
  • 在 Service 方法中,多次调用不同数据源的 Mapper
  • 第一个 Mapper 调用完成后,数据源就被清除
  • 后续调用都使用默认数据源
  1. 事务边界问题
  • Service 方法上有 @Transactional 注解
  • 在事务内多次切换数据源,原实现无法正确维护数据源状态

解决方案

修改后的 DataSourceAspect 实现

文件路径: ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DataSourceAspect.java

@Around("dsPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
    DataSource dataSource = getDataSource(point);
    
    // ✅ 关键改进 1: 记录当前数据源
    String oldDataSourceType = DynamicDataSourceContextHolder.getDataSourceType();
    boolean isNewDataSource = false;

    if (StringUtils.isNotNull(dataSource)) {
        String newDataSourceType = dataSource.value().name();
        // ✅ 关键改进 2: 只有当数据源发生变化时才设置新的数据源
        if (!newDataSourceType.equals(oldDataSourceType)) {
            DynamicDataSourceContextHolder.setDataSourceType(newDataSourceType);
            isNewDataSource = true;
            logger.debug("切换数据源: {} -> {}", oldDataSourceType, newDataSourceType);
        }
    }

    try {
        return point.proceed();
    }
    finally {
        // ✅ 关键改进 3: 只有当本次调用改变了数据源时,才需要恢复
        if (isNewDataSource) {
            // 恢复到之前的数据源
            if (StringUtils.isNotEmpty(oldDataSourceType)) {
                DynamicDataSourceContextHolder.setDataSourceType(oldDataSourceType);
                logger.debug("恢复数据源: {}", oldDataSourceType);
            } else {
                DynamicDataSourceContextHolder.clearDataSourceType();
                logger.debug("清除数据源,恢复到默认数据源");
            }
        }
    }
}

核心改进点

1. 记录旧数据源状态

String oldDataSourceType = DynamicDataSourceContextHolder.getDataSourceType();
  • 在切换数据源之前,先保存当前数据源类型
  • 用于后续恢复

2. 条件性切换数据源

if (!newDataSourceType.equals(oldDataSourceType)) {
    DynamicDataSourceContextHolder.setDataSourceType(newDataSourceType);
    isNewDataSource = true;
}
  • 只有当数据源确实需要切换时才进行切换
  • 通过 isNewDataSource 标记记录是否进行了切换

3. 智能恢复数据源

if (isNewDataSource) {
    if (StringUtils.isNotEmpty(oldDataSourceType)) {
        // 恢复到之前的数据源
        DynamicDataSourceContextHolder.setDataSourceType(oldDataSourceType);
    } else {
        // 之前没有设置数据源,清除当前数据源
        DynamicDataSourceContextHolder.clearDataSourceType();
    }
}
  • 只有在本次调用确实改变了数据源时,才进行恢复
  • 如果之前有数据源,恢复到之前的数据源
  • 如果之前是默认数据源,清除当前数据源

修复效果

修复前

Service.syncBranchDepartments() 开始
  ├─> departmentSyncMapper.selectBranchDepartments()
  │     ├─ 切换到 SQL Server ✅
  │     ├─ 执行查询 ✅
  │     └─ 清除数据源 ❌ (finally 块)
  │
  ├─> sysDeptMapper.checkDeptNameUnique()
  │     └─ 使用默认数据源 MySQL ✅
  │
  ├─> sysDeptMapper.insertDept()
  │     └─ 使用默认数据源 MySQL ✅
  │
  └─> 如果再次调用 departmentSyncMapper
        └─ 使用默认数据源 MySQL ❌ (应该是 SQL Server)

修复后

Service.syncBranchDepartments() 开始
  ├─> departmentSyncMapper.selectBranchDepartments()
  │     ├─ 当前数据源: null (默认)
  │     ├─ 切换到 SQL Server ✅
  │     ├─ 执行查询 ✅
  │     └─ 恢复到默认数据源 ✅ (因为之前是 null)
  │
  ├─> sysDeptMapper.checkDeptNameUnique()
  │     ├─ 当前数据源: null (默认 MySQL)
  │     ├─ 无需切换 ✅
  │     └─ 执行查询 ✅
  │
  ├─> departmentSyncMapper.selectBranchDepartments() (再次调用)
  │     ├─ 当前数据源: null (默认)
  │     ├─ 切换到 SQL Server ✅
  │     ├─ 执行查询 ✅
  │     └─ 恢复到默认数据源 ✅
  │
  └─> sysDeptMapper.insertDept()
        ├─ 当前数据源: null (默认 MySQL)
        ├─ 无需切换 ✅
        └─ 执行插入 ✅

数据源切换状态机

stateDiagram-v2
    [*] --> 默认数据源(MySQL)
    
    默认数据源(MySQL) --> SQL_Server: departmentSyncMapper调用
    SQL_Server --> 默认数据源(MySQL): 方法执行完成,恢复
    
    默认数据源(MySQL) --> 默认数据源(MySQL): sysDeptMapper调用(无切换)
    
    SQL_Server --> SQL_Server: 嵌套调用SQL_Server_Mapper(无切换)
    
    默认数据源(MySQL) --> [*]: Service方法结束

支持的场景

✅ 场景 1: 单次数据源切换

@Transactional
public void method1() {
    // 切换到 SQL Server
    List<Data> data = sqlServerMapper.selectData();
    
    // 自动恢复到 MySQL
    mysqlMapper.insertData(data);
}

✅ 场景 2: 多次交替切换

@Transactional
public void method2() {
    // 切换到 SQL Server
    List<Data1> data1 = sqlServerMapper.selectData1();
    
    // 恢复到 MySQL
    mysqlMapper.process(data1);
    
    // 再次切换到 SQL Server
    List<Data2> data2 = sqlServerMapper.selectData2();
    
    // 再次恢复到 MySQL
    mysqlMapper.saveAll(data2);
}

✅ 场景 3: 嵌套调用同一数据源

@Transactional
public void method3() {
    // 切换到 SQL Server
    List<Dept> depts = sqlServerMapper.selectDepartments();
    
    for (Dept dept : depts) {
        // 仍然是 SQL Server (不会重复切换)
        List<User> users = sqlServerMapper.selectUsersByDept(dept.getId());
        
        // 恢复到 MySQL
        mysqlMapper.saveUsers(users);
    }
}

✅ 场景 4: Service 间调用

@Service
public class ServiceA {
    @Autowired
    private ServiceB serviceB;
    
    @Transactional
    public void methodA() {
        // 默认 MySQL
        mysqlMapper.query();
        
        // 调用 ServiceB
        serviceB.methodB();
        
        // 仍然是 MySQL
        mysqlMapper.insert();
    }
}

@Service
public class ServiceB {
    public void methodB() {
        // 切换到 SQL Server
        sqlServerMapper.query();
        
        // 方法结束后恢复到调用者的数据源 (MySQL)
    }
}

调试日志

修改后的切面会输出详细的调试日志,帮助追踪数据源切换:

2025-10-18 10:00:01.123 DEBUG [DataSourceAspect] 切换数据源: null -> SQLSERVER
2025-10-18 10:00:01.456 DEBUG [DynamicDataSourceContextHolder] 切换到SQLSERVER数据源
2025-10-18 10:00:01.789 DEBUG [DataSourceAspect] 清除数据源,恢复到默认数据源

2025-10-18 10:00:02.123 DEBUG [DataSourceAspect] 切换数据源: null -> SQLSERVER
2025-10-18 10:00:02.456 DEBUG [DynamicDataSourceContextHolder] 切换到SQLSERVER数据源
2025-10-18 10:00:02.789 DEBUG [DataSourceAspect] 清除数据源,恢复到默认数据源

验证方法

1. 添加调试日志

在 Service 方法中添加数据源状态日志:

@Transactional
public AjaxResult syncBranchDepartments() {
    log.info("开始同步,当前数据源: {}", 
        DynamicDataSourceContextHolder.getDataSourceType());
    
    // 查询 SQL Server
    List<DepartmentSyncDTO> data = departmentSyncMapper.selectBranchDepartments();
    log.info("查询 SQL Server 完成,当前数据源: {}", 
        DynamicDataSourceContextHolder.getDataSourceType());
    
    // 写入 MySQL
    sysDeptMapper.insertDept(dept);
    log.info("写入 MySQL 完成,当前数据源: {}", 
        DynamicDataSourceContextHolder.getDataSourceType());
}

2. 单元测试

@Test
public void testDataSourceSwitch() {
    // 测试数据源切换
    AjaxResult result = departmentSyncService.syncBranchDepartments();
    
    // 验证结果
    assertEquals(200, result.get("code"));
}

3. 手动测试

# 执行同步接口
POST http://localhost:8080/system/dept/sync/branch

# 查看日志输出
tail -f logs/ruoyi-admin.log | grep -E "数据源|DataSource"

注意事项

⚠️ 事务管理

  • 修复后的切面支持在同一个事务中切换数据源
  • 但要注意:**不同数据源的操作不在同一个分布式事务中**
  • SQL Server 的查询和 MySQL 的写入是**两个独立的数据库连接**

⚠️ 性能影响

  • 每次 Mapper 调用都会经过 AOP 切面
  • 增加了数据源状态的判断和记录
  • 性能影响极小(纳秒级别)

⚠️ 线程安全

  • 使用 ThreadLocal 存储数据源类型
  • 每个线程有独立的数据源上下文
  • 不会影响其他线程

相关文件

文件 说明
DataSourceAspect.java 数据源切换切面(已修复)
DynamicDataSourceContextHolder.java 数据源上下文持有者
DepartmentSyncMapper.java 部门同步 Mapper(标注 @DataSource)
UserSyncMapper.java 用户同步 Mapper(标注 @DataSource)
DepartmentSyncServiceImpl.java 部门同步 Service(使用多数据源)

总结

问题根因

原始实现无条件清除数据源,导致嵌套调用时数据源状态丢失。

解决方案

记录旧数据源状态,条件性切换,智能恢复数据源。

修复效果

✅ 支持同一 Service 中多次切换数据源
✅ 支持嵌套调用
✅ 支持 Service 间调用
✅ 完全兼容原有功能

验证状态

✅ 代码已修改
⏳ 等待测试验证