# 多数据源切换问题修复说明 ## 问题描述 在同一个 Service 方法中调用多个不同数据源的 Mapper 时,出现**数据源切换失效**的问题: ```java @Service public class DepartmentSyncServiceImpl { @Autowired private DepartmentSyncMapper departmentSyncMapper; // @DataSource(SQLSERVER) @Autowired private SysDeptMapper sysDeptMapper; // 默认 MySQL @Transactional public AjaxResult syncBranchDepartments() { // 第一步:从 SQL Server 查询数据 List data = departmentSyncMapper.selectBranchDepartments(); // ✅ 这里能正确查询 SQL Server // 第二步:写入 MySQL sysDeptMapper.insertDept(dept); // ❌ 问题:这里也去了 MySQL,因为数据源被清除了 } } ``` **现象**: - 第一个 Mapper 调用能够正确切换到 SQL Server - 调用完成后,数据源被清除 - 后续的 MySQL Mapper 调用正常 - **但如果再次调用 SQL Server Mapper,会失败(使用了 MySQL 数据源)** --- ## 问题根因分析 ### 原始的 DataSourceAspect 实现 ```java @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) 2. **不支持嵌套调用**: - 在 Service 方法中,多次调用不同数据源的 Mapper - 第一个 Mapper 调用完成后,数据源就被清除 - 后续调用都使用默认数据源 3. **事务边界问题**: - Service 方法上有 `@Transactional` 注解 - 在事务内多次切换数据源,原实现无法正确维护数据源状态 --- ## 解决方案 ### 修改后的 DataSourceAspect 实现 **文件路径**: `ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DataSourceAspect.java` ```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. 记录旧数据源状态 ```java String oldDataSourceType = DynamicDataSourceContextHolder.getDataSourceType(); ``` - 在切换数据源之前,先保存当前数据源类型 - 用于后续恢复 #### 2. 条件性切换数据源 ```java if (!newDataSourceType.equals(oldDataSourceType)) { DynamicDataSourceContextHolder.setDataSourceType(newDataSourceType); isNewDataSource = true; } ``` - 只有当数据源确实需要切换时才进行切换 - 通过 `isNewDataSource` 标记记录是否进行了切换 #### 3. 智能恢复数据源 ```java 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) ├─ 无需切换 ✅ └─ 执行插入 ✅ ``` --- ## 数据源切换状态机 ```mermaid stateDiagram-v2 [*] --> 默认数据源(MySQL) 默认数据源(MySQL) --> SQL_Server: departmentSyncMapper调用 SQL_Server --> 默认数据源(MySQL): 方法执行完成,恢复 默认数据源(MySQL) --> 默认数据源(MySQL): sysDeptMapper调用(无切换) SQL_Server --> SQL_Server: 嵌套调用SQL_Server_Mapper(无切换) 默认数据源(MySQL) --> [*]: Service方法结束 ``` --- ## 支持的场景 ### ✅ 场景 1: 单次数据源切换 ```java @Transactional public void method1() { // 切换到 SQL Server List data = sqlServerMapper.selectData(); // 自动恢复到 MySQL mysqlMapper.insertData(data); } ``` ### ✅ 场景 2: 多次交替切换 ```java @Transactional public void method2() { // 切换到 SQL Server List data1 = sqlServerMapper.selectData1(); // 恢复到 MySQL mysqlMapper.process(data1); // 再次切换到 SQL Server List data2 = sqlServerMapper.selectData2(); // 再次恢复到 MySQL mysqlMapper.saveAll(data2); } ``` ### ✅ 场景 3: 嵌套调用同一数据源 ```java @Transactional public void method3() { // 切换到 SQL Server List depts = sqlServerMapper.selectDepartments(); for (Dept dept : depts) { // 仍然是 SQL Server (不会重复切换) List users = sqlServerMapper.selectUsersByDept(dept.getId()); // 恢复到 MySQL mysqlMapper.saveUsers(users); } } ``` ### ✅ 场景 4: Service 间调用 ```java @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) } } ``` --- ## 调试日志 修改后的切面会输出详细的调试日志,帮助追踪数据源切换: ```log 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 方法中添加数据源状态日志: ```java @Transactional public AjaxResult syncBranchDepartments() { log.info("开始同步,当前数据源: {}", DynamicDataSourceContextHolder.getDataSourceType()); // 查询 SQL Server List data = departmentSyncMapper.selectBranchDepartments(); log.info("查询 SQL Server 完成,当前数据源: {}", DynamicDataSourceContextHolder.getDataSourceType()); // 写入 MySQL sysDeptMapper.insertDept(dept); log.info("写入 MySQL 完成,当前数据源: {}", DynamicDataSourceContextHolder.getDataSourceType()); } ``` ### 2. 单元测试 ```java @Test public void testDataSourceSwitch() { // 测试数据源切换 AjaxResult result = departmentSyncService.syncBranchDepartments(); // 验证结果 assertEquals(200, result.get("code")); } ``` ### 3. 手动测试 ```bash # 执行同步接口 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 间调用 ✅ 完全兼容原有功能 ### 验证状态 ✅ 代码已修改 ⏳ 等待测试验证