MyBatis-Plus多数据源避坑指南为什么你的DS注解不生效5种常见场景解析最近在几个项目里折腾MyBatis-Plus的多数据源发现不少同事包括我自己刚开始都踩过DS注解不生效的坑。明明代码里标注了要用哪个数据源运行时却死活切不过去最后查日志发现SQL全跑默认库去了。这种问题在微服务架构或者读写分离场景下尤其恼人看似简单的配置背后其实藏着不少细节。今天我就结合自己踩坑和填坑的经历聊聊那些导致DS注解“罢工”的典型场景希望能帮你快速定位问题。1. 事务传播机制引发的“数据源锁定”这可能是最隐蔽也最常见的一个坑。很多开发者包括我最初都以为只要在Service方法上加了DS(slave)这个方法里的所有数据库操作就会自动路由到从库。但在Spring的事务管理框架下事情没那么简单。Spring的Transactional注解默认的传播行为是Propagation.REQUIRED。这意味着如果当前已经存在一个事务方法会加入该事务而不是新开一个。问题就出在这里数据源的选择是在事务开始时确定的。一旦事务开启整个事务生命周期内使用的数据源就被“锁定”了。想象这样一个场景你在一个标记了Transactional的masterService.methodA()里调用了另一个Service的slaveService.methodB()而methodB上标注了DS(slave)。你期望methodB里的查询能走到从库但实际上很可能不会。// MasterService.java Service public class MasterService { Autowired private SlaveService slaveService; Transactional // 默认使用主数据源开启事务 public void businessProcess() { // 一些在主库上的操作... insertToMaster(); // 调用从库服务期望查询从库 slaveService.queryFromSlave(); // 此处DS可能失效 } } // SlaveService.java Service public class SlaveService { DS(slave) // 期望切换到从数据源 public ListData queryFromSlave() { return mapper.selectList(null); // 实际可能仍在主库事务中执行 } }为什么失效因为businessProcess方法开启了一个主库事务。当执行到queryFromSlave时由于传播机制是REQUIREDqueryFromSlave方法并不会新启事务而是加入已有的主库事务。此时DS注解的切换逻辑在事务管理器看来已经“为时已晚”数据源早在第一个Transactional处就确定了。注意dynamic-datasource框架的数据源路由逻辑即决定用哪个库通常是通过一个DataSource包装器在获取连接时执行的。如果调用链处于同一个物理事务中那么第一次获取连接后后续操作可能会复用这个连接导致DS切换无效。解决方案对于必须要在事务内切换数据源的场景可以考虑以下两种策略使用REQUIRES_NEW传播级别强制被调用的方法开启一个新事务新事务会重新进行数据源选择。Service public class SlaveService { DS(slave) Transactional(propagation Propagation.REQUIRES_NEW) // 关键在此 public ListData queryFromSlave() { return mapper.selectList(null); } }优点简单直接能确保切换。缺点创建了新事务如果外层事务回滚内层REQUIRES_NEW的事务已独立提交可能造成数据不一致需谨慎评估业务逻辑。将查询操作移到事务外部这是更清晰的做法。重新设计调用链让不涉及写操作的、需要访问不同数据源的查询在事务边界之外进行。Service public class MasterService { Autowired private SlaveService slaveService; public void businessProcess() { // 先执行非事务性的从库查询 ListData dataList slaveService.queryFromSlave(); // 再开启事务进行主库的写操作 processWithTransaction(dataList); } Transactional private void processWithTransaction(ListData dataList) { // 主库操作... } }2. AOP执行顺序的“优先级陷阱”Spring AOP面向切面编程是Spring框架的基石事务管理Transactional和数据源路由DS都是通过AOP代理实现的。当多个AOP增强Advice作用在同一个方法上时它们的执行顺序就至关重要。如果顺序不对DS的切换信号可能无法被正确接收。默认情况下Spring AOP的执行顺序由切面Bean的优先级或Order注解决定但有时自动配置的顺序可能不符合我们的预期。Transactional的切面通常具有较高的优先级order值较小执行较早以确保在事务内执行。如果DS注解对应的切面在Transactional之后执行那么事务已经基于默认数据源开启了DS的切换就失效了。如何排查和解决首先你可以通过开启Debug日志来观察Bean的创建和AOP代理的顺序# application.yml logging: level: org.springframework.aop: DEBUG org.springframework.transaction: DEBUG com.baomidou.dynamic: DEBUG在日志中你可能会看到关于“Advisor”或“Proxy”的创建信息。更直接的解决方式是显式定义切面顺序。方案一通过配置属性指定顺序在application.yml中为dynamic-datasource指定一个明确的order值确保它比事务切面更早执行即order值更小数值越小优先级越高。spring: datasource: dynamic: order: -1 # 设置数据源切面的顺序使其在事务切面之前执行-1是一个常用的值因为它通常早于Spring默认的事务切面order值通常是Integer.MAX_VALUE或Ordered.LOWEST_PRECEDENCE。方案二理解默认行为实际上dynamic-datasource-spring-boot-starter从某个版本开始已经默认将其切面order设置为-1以尽力确保优先级。但如果你使用的是老版本或者项目中引入了其他自定义切面干扰了顺序手动配置一下是稳妥的做法。一个简单的验证顺序的方法在一个同时有DS和Transactional的方法里打上断点观察调用栈。你会看到类似DynamicDataSourceAnnotationInterceptor处理DS和TransactionInterceptor处理Transactional的调用。谁在前谁就先生效。3. 配置疏漏与命名歧义有时候问题不出在代码逻辑而出在基础的配置上。下面这个表格梳理了配置环节容易出错的几个点问题类型错误示例/表现正确做法/排查点依赖缺失或版本冲突项目启动正常但DS注解完全无效果日志无相关路由信息。1. 检查pom.xml或build.gradle确保引入了正确的startercom.baomidou:dynamic-datasource-spring-boot-starter。2. 检查版本是否与Spring Boot、MyBatis-Plus兼容。建议使用官方文档推荐的版本组合。YAML/Properties配置错误数据源定义在错误的配置键下多数据源名称与DS注解值不匹配。1. 确认配置前缀是spring.datasource.dynamic。2. 检查datasource节点下的子数据源如master,slave_1定义是否正确包括url,username,password。3.严格核对DS(slave_1)中的slave_1必须与配置中的slave_1:完全一致大小写敏感。未设置主数据源primary应用启动报错提示找不到主数据源。在配置中明确指定primary: master或其他你定义的主数据源名称。组件扫描问题自定义的Mapper或Service所在的包未被Spring扫描到导致DS注解未被代理。1. 确保SpringBootApplication或MapperScan注解能扫描到你的Mapper接口。2. 确保Service类在Spring容器管理的包路径下。这里给出一份相对完整的、清晰的配置示例你可以对照检查spring: datasource: dynamic: primary: master # 明确指定默认数据源 strict: false # 建议false未匹配时使用默认数据源而非抛异常 datasource: master: url: jdbc:mysql://localhost:3306/master_db?useUnicodetruecharacterEncodingutf8 username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver slave_1: # 注意这个名称在代码中要完全一致 url: jdbc:mysql://localhost:3307/slave_db?useUnicodetruecharacterEncodingutf8 username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver # 可选设置AOP切面顺序确保先于事务切面执行 # order: -14. 方法内部调用导致的代理失效这是一个经典的Spring AOP代理问题不仅影响DS也影响Transactional、Cacheable等所有基于代理的Spring功能。Spring AOP是通过代理对象来实现的。当你调用一个Bean的另一个方法时如果走的是this.内部方法()的路径那么这次调用会绕过代理对象直接调用目标对象的原始方法导致切面逻辑包括数据源切换失效。看这个例子Service public class UserService { DS(slave) public User getFromSlave(Long id) { return userMapper.selectById(id); } public User getFromSlaveWrapper(Long id) { // 错误内部调用DS失效 return this.getFromSlave(id); } }在getFromSlaveWrapper中直接调用this.getFromSlave(id)DS注解不会生效查询会使用默认数据源。解决方案将需要切换数据源的方法抽取到另一个Service中这是最清晰、最推荐的方式。通过注入另一个Service的Bean来调用确保走代理。Service public class SlaveQueryService { DS(slave) public User getFromSlave(Long id) { return userMapper.selectById(id); } } Service public class UserService { Autowired private SlaveQueryService slaveQueryService; // 注入Bean public User getFromSlaveWrapper(Long id) { // 正确通过代理对象调用 return slaveQueryService.getFromSlave(id); } }使用ApplicationContext获取代理对象不推荐较复杂通过AopContext.currentProxy()获取当前对象的代理然后进行调用。这种方法需要额外配置EnableAspectJAutoProxy(exposeProxy true)并且代码不够优雅。Service public class UserService { DS(slave) public User getFromSlave(Long id) { return userMapper.selectById(id); } public User getFromSlaveWrapper(Long id) { // 获取当前代理对象再调用 UserService proxy (UserService) AopContext.currentProxy(); return proxy.getFromSlave(id); } }5. 多数据源与MyBatis原生组件混用的冲突如果你的项目在引入dynamic-datasource的同时还手动配置了多个原生的MyBatisSqlSessionFactory或DataSource极有可能产生冲突。Spring容器中如果存在多个同类型的Bean而dynamic-datasource的自动配置没有正确接管就会导致DS路由失败。典型症状应用启动时控制台可能出现多个DataSource或SqlSessionFactoryBean的日志或者在使用DS时抛出不支持动态数据源的异常。排查与解决思路检查自动配置确保你没有使用EnableTransactionManagement等注解手动覆盖了事务管理器或者通过Bean手动定义了一个DataSource。dynamic-datasource-starter会自动配置它所需的一切Bean如DynamicRoutingDataSource,DataSourceTransactionManager等。手动定义可能会覆盖这些自动配置。检查排除项如果你确实需要保留一些自定义配置确保它们与动态数据源兼容。查看DynamicDataSourceAutoConfiguration类了解它自动注册了哪些Bean避免重复定义。简化配置对于大多数场景强烈建议只使用dynamic-datasource这一套多数据源方案不要再混合使用Spring Boot原生的多数据源配置方式。将原有的、分散的DataSource、SqlSessionFactory配置全部移除统一到spring.datasource.dynamic配置项下管理。一个常见的冲突案例是同时存在以下配置// 错误的配置示例手动定义了主数据源Bean Configuration public class DataSourceConfig { Bean Primary ConfigurationProperties(prefix spring.datasource.master) public DataSource masterDataSource() { return DataSourceBuilder.create().build(); // 这会干扰dynamic-datasource } }上述代码定义了一个名为masterDataSource的Bean并被标记为Primary这很可能“劫持”了数据源的默认选择权导致dynamic-datasource的自动路由失效。正确的做法是删除这类手动配置的DataSourceBean完全交由dynamic-datasource-starter来管理。最后再分享一个调试小技巧开启dynamic-datasource的调试日志可以清晰地看到每次SQL执行时选择数据源的逻辑。logging: level: com.baomidou.dynamic.datasource: DEBUG这样在日志中搜索“DynamicDataSource”你就能看到类似“Switching DataSource to [slave_1]”的信息从而直观地判断DS是否生效以及生效后具体切换到了哪个数据源。