跨数据源操作
在MyBatis-Plus中实现跨数据源查询,最简便的方法是使用官方推荐的dynamic-datasource-spring-boot-starter
组件。它允许你通过一个简单的@DS
注解来切换数据源。下面是一个详细的配置和使用指南。后面也会介绍下DynamicDataSourceContextHolder
进行数据源切换。
主要步骤:
- 添加依赖, 引入
dynamic-datasource-spring-boot-starter
- 配置数据源,在
application.yml
中配置master
和busiz
数据源,并设置默认项 - 使用注解 在方法或类上使用
@DS("busiz")
注解切换数据源 - 注意事项 注意注解生效的优先级和事务相关的问题
详细步骤
1. 添加Maven依赖
在你的pom.xml文件中添加以下依赖:
<dependency><groupId>com.baomidou</groupId><artifactId>dynamic-datasource-spring-boot-starter</artifactId><version>3.6.1</version> <!-- 建议使用最新版本 -->
</dependency>
请确保版本与你的MyBatis-Plus版本兼容 。
配置多数据源
在application.yml
配置文件中,配置你的master
(默认)和busiz
数据源。
spring:datasource:dynamic:primary: master # 设置 master 为你的默认数据源 strict: false # 设置严格模式,默认false不启动. 启动后在未匹配到指定数据源时候会抛出异常,不启动则使用默认数据源.datasource:master:url: jdbc:mysql://localhost:3306/master_db?useUnicode=true&characterEncoding=utf8username: rootpassword: rootdriver-class-name: com.mysql.cj.jdbc.Driverbusiz:url: jdbc:mysql://localhost:3306/busiz_db?useUnicode=true&characterEncoding=utf8username: rootpassword: rootdriver-class-name: com.mysql.cj.jdbc.Driver
使用@DS注解切换数据源
在你需要查询busiz
数据源的Service实现类或方法上使用@DS("busiz")
注解。
注解在类上:该类所有方法默认使用busiz
数据源。
@Service
@DS("busiz") // 整个类默认使用busiz数据源
public class UserServiceImpl implements UserService {// ... class body
}
注解在方法上:仅该方法使用busiz
数据源。方法上的注解优先级高于类上的注解 。
@Service
public class UserServiceImpl implements UserService {@Override@DS("busiz") // 此方法使用busiz数据源public List<User> selectFromBusiz() {// 你的查询逻辑return userMapper.selectList(null);}// 没有注解,使用默认的master数据源public List<User> selectFromMaster() {// 你的查询逻辑return userMapper.selectList(null);}
}
重要注意事项
事务管理器问题: 在同一个Service类中,A方法(无论有无@DS注解)调用B方法(有@DS注解),B方法的注解可能不生效。这通常是因为Spring AOP代理的机制。解决方法是将需要切换数据源的方法放到另一个Service类中调用 。
事务与数据源切换: 避免在需要切换数据源的方法上使用@Transactional注解。如果必须使用事务,需要在@Transactional注解中显式指定事务管理器,例如 @Transactional(rollbackFor = Exception.class, transactionManager = “dynamicDataSourceTransactionManager”) 。但更简单的做法是将操作不同数据源的事务拆分开。
数据源名称: 建议数据源名称不要包含下划线,以免引起无法切换的问题 。
注解位置: 官方强烈建议将@DS
注解加在Service
的实现类上,而不是Mapper接口上,以保证事务的正常运作 。
问题
问题1
A方法(无论有无@DS注解)调用B方法(有@DS注解),B方法的注解可能不生效。有时候可能会数据源切换不过去。
答:在同一个Service类中,A方法调用B方法时,B方法的@DS注解确实可能不生效,导致数据源切换失败。
问题根源
这个问题源于 Spring 的AOP(面向切面编程)机制
的工作原理:
@DS
注解本质上是通过Spring AOP
实现的,Spring AOP默认使用动态代理来增强方法,在同一个类内部的方法调用,不会经过代理对象,而是直接调用目标方法,因此AOP拦截器无法介入,@DS注解也就不会生效。
具体示例
@Service
public class UserServiceImpl implements UserService {@Autowiredprivate UserMapper userMapper;// A方法:没有@DS注解,应该使用默认数据源(master)public void methodA() {System.out.println("methodA - 应该使用默认master数据源");// 这里调用B方法,期望使用busiz数据源methodB(); // ❌ 实际:B方法的@DS注解不会生效,仍然使用master数据源// 其他逻辑...}// B方法:有@DS注解,期望使用busiz数据源@DS("busiz")public List<User> methodB() {System.out.println("methodB - 期望使用busiz数据源");return userMapper.selectList(null); // ❌ 实际:仍然使用busiz数据源}
}
解决方案
方案1: 将方法拆分到不同的Service类中(推荐)
// Service1
@Service
public class ServiceA {@Autowiredprivate ServiceB serviceB;public void methodA() {System.out.println("methodA - 使用默认master数据源");// 通过注入的ServiceB调用,会经过代理,@DS注解生效serviceB.methodB(); // 会切换到busiz数据源}
}// Service2
@Service
public class ServiceB {@Autowiredprivate UserMapper userMapper;@DS("busiz")public List<User> methodB() {System.out.println("methodB - 使用busiz数据源");return userMapper.selectList(null); }
}
方案2: 从ApplicationContext中获取代理对象
@Service
public class UserServiceImpl implements UserService {@Autowiredprivate ApplicationContext applicationContext;@Autowiredprivate UserMapper userMapper;public void methodA() {System.out.println("methodA - 使用默认master数据源");// 从Spring容器中获取代理对象来调用UserService proxy = applicationContext.getBean(UserService.class);proxy.methodB(); // ✅ 正确:@DS注解生效}@DS("busiz")public List<User> methodB() {System.out.println("methodB - 使用busiz数据源");return userMapper.selectList(null);}
}
优先使用方案1(方法拆分到不同Service),这是最清晰、最可靠的方式
在设计Service层时,将有不同数据源需求的方法自然地分布到不同的Service类中
如果确实需要在同一个Service中调用,考虑使用方案2
问题2
那在mapper层也可以用@DS注解吗
官方强烈不建议在Mapper层使用@DS注解,这可能会导致不可预期的问题。
为什么不建议在Mapper层使用
- 事务管理问题
// 不推荐的写法 - 在Mapper上使用@DS
@DS("busiz") // 官方不推荐这样做
@Mapper
public interface UserMapper extends BaseMapper<User> {List<User> selectFromMaster();
}
@Service
public class UserService {@Transactional // 事务注解public void businessMethod() {// 在事务中混合不同数据源的操作userMapper.selectFromBusiz(); // 使用busiz数据源userMapper.insert(user); // 使用默认master数据源// ❌ 事务一致性无法保证}
}
- 数据源切换混乱
多个Mapper使用不同数据源,在service层调用容易造成混乱
DynamicDataSourceContextHolder
代码含义解析
// 1. 将"sys"数据源标识压入栈中,切换到sys数据源
DynamicDataSourceContextHolder.push("sys");// 2. 在sys数据源上执行插入job的操作
jobService.insertJob(job);// 3. 从栈中弹出当前数据源标识,恢复到之前的数据源
DynamicDataSourceContextHolder.poll();
工作原理
数据源切换机制
-
push("sys")
: 将"sys"数据源标识压入线程局部的栈中 -
poll()
: 从栈中弹出当前数据源标识,恢复到前一个数据源
基于ThreadLocal实现,确保线程安全
执行流程
-
保存当前数据源上下文
-
切换到"sys"数据源
-
执行数据库操作
-
恢复原来的数据源
/必须清理数据源上下文,避免内存泄漏和数据源污染
DynamicDataSourceContextHolder.clear();
public void complexMultiDataSourceOperation() {List<User> busizUsers = null;List<User> masterUsers = null;try {// 切换到 busiz 数据源DynamicDataSourceContextHolder.push("busiz");busizUsers = userMapper.selectList(new LambdaQueryWrapper<User>().eq(User::getType, "GGG"));// 切换到master 数据源DynamicDataSourceContextHolder.push("master");masterUsers = userMapper.selectList(new LambdaQueryWrapper<User>().eq(User::getType, "CCC"));} finally {// 必须清理数据源上下文,避免内存泄漏和数据源污染DynamicDataSourceContextHolder.clear();}// 处理业务逻辑...}
}
批量操作多个数据源
@Service
public class BatchDataService {public void syncDataBetweenDataSources() {List<String> dataSources = Arrays.asList("ds1", "ds2", "ds3");for (String ds : dataSources) {try {DynamicDataSourceContextHolder.push(ds);// 从每个数据源读取数据List<User> users = userMapper.selectList(null);processUsers(users);} finally {DynamicDataSourceContextHolder.clear();}}}
}