☰
Spring Boot+MyBatis多数据源实战:动态路由与跨库事务详解
2026/10/17 4:27:43 网站建设 项目流程

真正让我下决心把多数据源这套东西整理成文的,是上个月帮一个朋友排查的线上问题:一个 Spring Boot 服务里明明配了两个数据库,service 层也按模块分了 Mapper,结果启动报错、Mapper 扫描打架、事务走错库,折腾到凌晨。事后回头看,问题不外乎两件事——DataSource 没有归属清楚,SqlSessionFactory 不知道该去哪拿连接。

Spring Boot 整合 MyBatis 做跨库操作,看起来是配置工作,本质上是“数据源调度”工作。同一个工程里到底该维护几个 SqlSessionFactory?是静态一库一套,还是用一个动态路由统一转发?跨库数据在应用层怎么组合、跨库事务怎么处理?这篇文章不打算给你抄一堆面试用的八股,而是用实际项目的配置过程,把这些机制彻底讲明白。无论是做读写分离,还是多模块分库,或者对接第三方只读库,这份脉络基本都能覆盖。

1. 多数据源到底在解决什么问题

先跳开代码,想想你在什么情况下会用到多个数据源。

最常见的是读写分离。MySQL 主库承担写入,从库承担查询,应用层根据方法语义把 SQL 发到不同连接。这个时候两个库往往结构一致,Mapper 是一样的,区别只是连接串和连接池。

第二类是业务分库。用户服务的数据在 user_db,订单服务的数据在 order_db,虽然都在同一个 Spring Boot 工程里,但表结构、Mapper、事务边界都独立。跨库操作不是在一个 SQL 里 join,而是在 service 层把两个库的查询结果组装起来。

第三类是报表库、历史库、第三方只读库。数据不在你的业务主库,也不是每张表都有对应实体,更多是临时查询、统计分析,或者同步任务。

不管是哪种场景,落到技术栈上,你都必须回答三个底层问题:

  • 每个数据源要有一个独立的 DataSource Bean,并且连接池互不干扰。
  • MyBatis 的 SqlSessionFactory 需要知道自己的数据源归属。一个 SqlSessionFactory 绑定一个 DataSource,这是 MyBatis 和 Spring 整合的基础规则。
  • 事务管理器必须明确绑定哪个数据源。Spring 的声明式事务默认只管理一个 Connection,不可能自动跨越两个物理库。

把这三个问题想透,后面所有配置都是围绕它们展开的。

2. 两种主流实现方案:多Factory与动态路由

我见过太多人一上来就抄动态数据源代码,但并不知道动态数据源和静态多数据源的区别。这两套方案的适用场景完全不一样,选错了后面全是坑。

2.1 静态多SqlSessionFactory:一库一套MyBatis配置

所谓静态多数据源,就是每个数据库对应一套独立的 Spring Bean 链:DataSource → SqlSessionFactory → SqlSessionTemplate → Mapper 扫描包。

举个例子。工程里有 user_db 和 order_db,我会建两个配置类:

  • UserDbConfig,扫描com.example.mapper.user包,绑定userSqlSessionFactory。
  • OrderDbConfig,扫描com.example.mapper.order包,绑定orderSqlSessionFactory。

使用的时候,UserMapper 只操作 user_db,OrderMapper 只操作 order_db。如果 order 列表需要关联 user 名称,实际是拿 order 表的数据查出 userId 列表,再去调 userMapper 批量查询,在 service 层做组装。

这套方案的优势是隔离非常干净。每个库的 Mapper、XML、下划线转驼峰配置、插件拦截器都可以独立设置,互相之间没有任何耦合。缺点也很明显,框架代码多,每加一个库就要多一套配置类;而且它不适合读写分离这种“同一个 Mapper 在不同连接上切换”的场景。如果想把 OrderMapper 在一个方法里查主库、另一个方法里查从库,静态方案做不到,因为它绑定死了。

2.2 动态路由:用AbstractRoutingDataSource做统一门面

动态路由的核心类就是AbstractRoutingDataSource。它是 Spring 提供的一个抽象类,内部维护一个目标数据源 Map,每次获取连接时,调用determineCurrentLookupKey()决定当前该用哪个 key 去 Map 里取真正的 DataSource。

画成一句话就是:你配了一个“门面” DataSource,业务代码都在这个门面上拿连接,但它会在内部帮你转发到真正的主库或从库。

这个门面配合 ThreadLocal 和自定义注解,就能做到方法级切换:

  • 进入 service 方法前,通过注解把目标数据源的 key 放进 ThreadLocal。
  • 门面在获取连接时,从 ThreadLocal 拿到 key,返回对应的真实 DataSource。
  • 方法执行完,必须在 finally 里清掉 ThreadLocal,否则线程池复用时 key 会串库。

动态方案只需要一个 SqlSessionFactory,一套 Mapper,但它可以连接任意多个配置好的数据源。读写分离场景最实用,同一个 OrderMapper,标注@DS("master")走主库,标注@DS("slave")走从库。

2.3 选型结论:项目该怎么选

对比维度静态多SqlSessionFactory动态路由
配置复杂度高,每库一套配置低,一套核心代码即可
Mapper与库绑定硬绑定,包路径划分靠注解/上下文动态决定
读写分离不适用非常合适
业务分库且Mapper明确拆分推荐可以做,但意义不大
多租户动态路由不适用推荐
运维排查难度清晰需要理解路由机制

我的建议很直接:如果只是读写分离,或者多库结构一样纯粹按流量分发,用动态路由。如果业务上已经按模块分库,每个模块的 Mapper、表结构完全独立,静态多 SqlSessionFactory 更清晰,因为它把“哪个库”固化在包结构里,团队里任何人看代码都不会搞错。

3. 基础搭建:注册多数据源并让MyBatis认账

不管选哪种方案,第一步都是把多个数据源注册成 Spring 容器里的 Bean。这里最大的坑是 Spring Boot 的自动配置,必须搞清楚如何处理。

3.1 依赖引入与自动配置排除

Maven 依赖按正常 Spring Boot + MyBatis 引入即可,连接池我常用 Druid:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.2</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.9</version> </dependency>

当工程里只有一个数据源时,DataSourceAutoConfiguration会帮你自动创建 DataSource,很省事。但只要你有两个及以上数据源,自动配置就是灾难源头:它并不清楚谁是主谁是次,扫描到多个连接池 Bean 时,注入点就会报歧义错误。

所以我通常在启动类上直接排除自动配置:

@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class }) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }

排除之后,所有数据源都由我自己的配置类显式创建。这样反而更好理解,不用和自动配置的魔法逻辑纠缠。

3.2 application.yml统一存放多库连接

连接信息推荐统一放在spring.datasource.dynamic前缀下,结构清晰,后续就算扩展到五六个库也不会乱。

spring: datasource: dynamic: master: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/user_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 slave: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/order_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 type: com.alibaba.druid.pool.DruidDataSource

注意,这里我没有写spring.datasource.url,因为一旦习惯多数据源,就不该让 Spring Boot 自动配置从spring.datasource.url读取单库信息了。用自定义前缀,自动配置想加载也无从下手。

3.3 数据源注册与MyBatis装配

如果是静态多 SqlSessionFactory 方案,配置类可以这么写。

先注册两个数据源:

@Configuration public class DataSourceConfig { @Bean @Primary @ConfigurationProperties(prefix = "spring.datasource.dynamic.master") public DataSource masterDataSource() { return DruidDataSourceBuilder.create().build(); } @Bean @ConfigurationProperties(prefix = "spring.datasource.dynamic.slave") public DataSource slaveDataSource() { return DruidDataSourceBuilder.create().build(); } }

然后分别给 user 包和 order 包创建自己的 SqlSessionFactory:

@Configuration @MapperScan( basePackages = "com.example.mapper.user", sqlSessionFactoryRef = "userSqlSessionFactory" ) public class UserMybatisConfig { @Bean public SqlSessionFactory userSqlSessionFactory( @Qualifier("masterDataSource") DataSource dataSource) throws Exception { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSource); factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources("classpath*:mapper/user/*.xml")); org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration(); configuration.setMapUnderscoreToCamelCase(true); factoryBean.setConfiguration(configuration); return factoryBean.getObject(); } @Bean public SqlSessionTemplate userSqlSessionTemplate( @Qualifier("userSqlSessionFactory") SqlSessionFactory factory) { return new SqlSessionTemplate(factory); } }

OrderMybatisConfig写法几乎一样,只是 Mapper 包换成com.example.mapper.order,数据源换成slaveDataSource,XML 路径换成mapper/order/*.xml。

这里补充两个很容易踩的细节。第一,数据源 Bean 必须加@Qualifier明确指定,否则 Spring 发现有多个 DataSource 会直接注入失败。第二,多 SqlSessionFactory 场景下,必须在其中一个工厂和对应的 SqlSessionTemplate 上加@Primary,否则@MapperScan虽然指定了工厂,但其他隐式注入点仍可能因为找到多个实例而报错。

4. 跨库操作核心:动态数据源切换完整落地

如果说静态方案的核心是“配置隔离”,动态方案的核心就是“路由切换”。这一套东西拆开看其实三个组件:路由 DataSource、ThreadLocal 存储、注解加 AOP 切面。

4.1 ThreadLocal与数据源路由的配合

先看路由数据和 ThreadLocal 的存储:

public class DynamicDataSource extends AbstractRoutingDataSource { private static final ThreadLocal<String> HOLDER = new ThreadLocal<>(); public static void setKey(String key) { HOLDER.set(key); } public static String getKey() { return HOLDER.get(); } public static void clearKey() { HOLDER.remove(); } @Override protected Object determineCurrentLookupKey() { return getKey(); } }

ThreadLocal 的作用非常重要。连接池线程和业务线程往往不是同一个,AbstractRoutingDataSource 只有在真正获取 Connection 时才会调用determineCurrentLookupKey(),这个调用发生在连接池的内部线程上。如果你用普通的成员变量存储 key,连接池多线程复用一把锁里面,你的 key 早就被覆盖了。ThreadLocal 能保证每个线程拿到的 key 是独立的,不会互相污染。

同时要注意,ThreadLocal 里存的只是 key,不是连接本身。真正打开到哪个数据库的连接,还是由各个真实 DataSource 自己维护。所以即便路由切换,连接池的活性连接数、事务隔离级别这些指标,也都还是各管各的。

4.2 自定义@DS注解+AOP切面

手工在每一个 mapper 方法里写DynamicDataSource.setKey()太啰嗦,而且容易漏掉 finally 清理。用注解加切面是主流的做法。

先定义注解:

@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DS { String value(); }

再写切面:

@Aspect @Component public class DataSourceAspect { @Around("@annotation(ds)") public Object switchDataSource(ProceedingJoinPoint point, DS ds) throws Throwable { DynamicDataSource.setKey(ds.value()); try { return point.proceed(); } finally { DynamicDataSource.clearKey(); } } }

切面做的事分三步:进入方法前放 key,执行原方法,finally 里清 key。为什么清理一定要放 finally?因为方法一旦抛异常,如果没有 finally,ThreadLocal 里的 key 就永远残留。线程池里的线程下一次处理其他任务时,就会拿旧 key 去路由,数据全查错库。这是动态数据源最高频的线上故障,没有之一。

AOP 切面对接口和实现类都有效,但有一个硬伤:同一个类里的方法自调用,this.orderService.query()调用this.queryFromSlave(),后者不会经过代理,切面完全不生效。遇到这种情况,要么把切换的方法拆到另一个 Service 类里,要么用AopContext.currentProxy()调代理对象。

4.3 跨库查询与读写分离示例

数据源装配时,把动态路由作为统一门面交给 MyBatis:

@Configuration public class DynamicDataSourceConfig { @Bean @Primary public DataSource dynamicDataSource( @Qualifier("masterDataSource") DataSource master, @Qualifier("slaveDataSource") DataSource slave) { Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put("master", master); targetDataSources.put("slave", slave); DynamicDataSource dynamicDataSource = new DynamicDataSource(); dynamicDataSource.setTargetDataSources(targetDataSources); dynamicDataSource.setDefaultTargetDataSource(master); return dynamicDataSource; } @Bean public SqlSessionFactory sqlSessionFactory( @Qualifier("dynamicDataSource") DataSource dataSource) throws Exception { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSource); factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources("classpath*:mapper/*.xml")); org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration(); configuration.setMapUnderscoreToCamelCase(true); factoryBean.setConfiguration(configuration); return factoryBean.getObject(); } }

这里的setDefaultTargetDataSource(master)很关键。确保没有注解的方法、代码里忘了设置 key 的时候,至少能稳定落到主库,不会因为取不到 key 直接报错。

使用的时候,注解打在 service 方法上:

@Service public class OrderReadService { @DS("slave") public OrderVO getOrderDetail(Long orderId) { Order order = orderMapper.selectById(orderId); User user = userMapper.selectById(order.getUserId()); return assemble(order, user); } }

这样一个方法里同时查到 order 和 user,刚好是跨库操作的实际形态:orderMapper 和 userMapper 虽然在同一个服务里操作,但底层连接来自不同数据库。动态路由保证 userMapper 的 key 和 orderMapper 的 key 都是同一个 slave。

5. 跨库操作工程化:联查、分页、并发与事务

跨库操作不是把数据查出来就完了。真实项目里还要面对联查、分页、性能、数据一致性这些问题。我把自己在项目里沉淀的对应方案记录在这里。

5.1 应用层做跨库联查的通用套路

以我们项目为例,报表服务需要把 user_db 的用户信息和 order_db 的订单汇总到一张报表里。数据库层面没有跨库 join,也不可能用 FEDERATED 表这种旁门左道来污染业务架构。我的做法是三层拼接:

  • 先在主库查出当前页的主体数据。比如分页查订单,先拿到当前页的订单记录和 userId 列表。
  • 去从库批量查关联数据。把 userId 列表去重,调 userMapper 批量查回用户信息,存进一个Map<Long, User>。
  • 在内存中组装。遍历订单列表,用 userId 从 Map 里取用户,拼成 VO 返回。

这套模式的核心是批量查询。绝对不要在循环里一条一条查关联库,那是最容易拖垮连接池的写法。先收集 ID,再 IN 查询,最后 Map 组装,大多数中小规模表都能跑得流畅。

5.2 批量回填与分页数据怎么办

分页情况下,最忌讳的做法是主库分页完,从库再逐条回填。假设每页 20 条,每条都要跨库查一次,就是 20 次网络往返,慢是必然的。

正确做法和联查套路一致:把回填字段真正需要的 ID 收集好,一次批量查完。用户名字段、商品名称字段、状态名称字段,都各自拼成一个批量查询,然后一次性回填。整个过程的数据库访问次数可以控制在 2 到 3 次,而不是 N 次。

有一个容易忽略的点是排序。跨库数据最后要按某个从库字段排序时,不要等全部回填完再内存排序。如果数据量大,内存排序会变成瓶颈。更稳妥的做法是先把需要排序的从库字段查出来,在内存里算好 ID 顺序,再回主库按这个顺序查分页数据。

5.3 多库并发查询提升吞吐

耗时监控数据显示,跨库接口的瓶颈往往就在串行查询连接库。一次接口要查 A 库和 B 库,A 库耗时 50ms,B 库耗时 80ms,串行下来就是 130ms。如果把两个查询丢到不同的线程池并行,总耗时基本能压到最长的那条 80ms 左右。

我在项目中是这样实现的:用一个固定大小的线程池,把两个独立查询任务包装成CompletableFuture,最后allOf().join()或者thenCombine合并结果。

CompletableFuture<List<Order>> orderFuture = CompletableFuture.supplyAsync(() -> orderMapper.selectList(condition), queryExecutor); CompletableFuture<List<User>> userFuture = CompletableFuture.supplyAsync(() -> userMapper.selectBatchIds(userIds), queryExecutor); CompletableFuture.allOf(orderFuture, userFuture).join();

但这里有一个值得警惕的坑:动态数据源的 ThreadLocal 无法跨线程传递。如果你在异步线程里执行 Mapper,必须把数据源 key 在 submit 前显式传给子线程,比如用TaskDecorator包装任务,在线程执行前设置 key,执行后清理。否则子线程拿到的是父线程遗留的 key,或者干脆是 null,路由会落空。

5.4 事务边界:为什么跨库别开大事务

多数据源下的事务,是比路由更复杂的问题,也是出错最多的环节。

常规@Transactional用的是DataSourceTransactionManager,它绑定某一个 DataSource,事务开启时就会从那个数据源拿到一个连接并缓存住,整个事务期间所有 SQL 都走同一条连接。所以你会发现一个经典现象:方法上既标了@Transactional,又标了@DS("slave"),执行时可能完全不生效,或者事务里的读操作仍然走主库。

因为 Spring 事务拦截器和 AOP 切面的执行顺序是有讲究的。如果事务管理器先于数据源切面执行,它已经打开主库连接,后面你切到 slave,事务内部拿到的还是主库连接。更特殊的是,MyBatis 的 SqlSession 也已经和后缀事务绑定,路由 key 变得没有意义。

所以动态切换数据源,和事务是天然冲突的。如果你只是做查询,最好别加@Transactional,让连接池按需取连接。如果确实需要跨库写入保证一致性,就要引入分布式事务方案。轻量级场景可以用本地消息表加对账补偿,一致性要求极高的金融类场景再考虑 Seata、Atomikos 这类方案。这些都是分布式事务的话题,本文不展开,但你必须知道:Spring Boot 默认是做不到跨库强一致的,别指望一个注解搞定。

6. 多数据源高频故障排查手记

这部分是实战中最容易碰到的报错和诡异现象,我按排查思路整理成一份速查表。

现象报错/表现原因解决方向
启动失败No qualifying bean of type DataSource多个数据源 Bean 注入歧义主数据源/路由数据源加 @Primary
启动失败mapper bean not found / already boundMapper 扫描包和 SqlSessionFactory 绑定错误检查 @MapperScan 的 basePackages 与 sqlSessionFactoryRef
切库不生效查询走了主库或走错库切面没生效、自调用、AOP 顺序问题确认切面被扫描,拆开代理,或调整 @Order
数据串库A 业务请求查到了 B 库数据ThreadLocal 没有 finally 清理,线程池复用清理逻辑放 finally,必要时用任务装饰器
事务内切换失效@DS 在事务方法里不生效事务拦截器先拿到连接,路由被忽略去掉事务,或换分布式事务方案
连接池耗尽wait millis timeout / 连接不可用连接不是在 finally 里释放,或跨库循环查询检查代码关闭连接,改造批量查询和并发

下面挑三个最典型的展开讲。

6.1 启动失败:No qualifying bean of type DataSource

这个报错几乎每个多数据源项目都见过。原因是 Spring 自动装配时,发现有多个 DataSource 类型的 Bean,但注入处没有明确指出要用哪一个。

解决办法有两步:第一步把默认的数据源(通常是主库,或者动态路由门面)加上@Primary;第二步在所有注入点用@Qualifier指定 Bean 名。有人觉得@Primary就够了,但实际上某些底层组件需要拿精确 Bean,比如DataSourceTransactionManager构造时就要显式传入。所以两重保险都加上,最省事。

6.2 @DS注解不生效与ThreadLocal脏数据

最常见的是自调用问题。Spring AOP 创建的是代理对象,你从外部注入的 Service 是代理,但方法内部调用this.xxx()时,this 是原始对象,不是代理,注解切面根本就不会执行。

排查方法很简单,在切面里打个日志,看@Around有没有被触发。如果没触发,基本就是自调用或没扫描到切面。自调用的解决方案前面提过,拆到独立 Bean 或使用AopContext.currentProxy()。

ThreadLocal 脏数据排查则是看“串库”现象。系统运行一段时间后,某个请求的数据莫名变成另一个库的。最直接的排查办法是在determineCurrentLookupKey()里打上 key 日志,同时检查所有setKey和clearKey是否成对出现。

6.3 事务内切换数据源失效

这个问题犯的人最多,因为它不是报错,而是悄悄出数据问题。

一个 service 方法标了@Transactional,方法内部调用了@DS("slave")的另一个方法,理论上想读从库,实际读到的还是主库。原因就是事务连接已经打开,路由切换不会改变已有连接。

我的建议是把“读”和“写”拆到不同的 service 和事务边界。写操作放主库,只做查询的放从库且不加事务。不要让@Transactional和@DS同时出现在一个方法上。如果确实要在一个方法里先读从库、再写主库,那就把读从库的方法拆出去,先执行完再进入带事务的写方法,哪怕这样会牺牲一点点一致性,也比事务内路由失效强得多。

6.4 Mapper扫描冲突与多SqlSessionFactory注入问题

静态多数据源方案里,两个@MapperScan最容易扫到同一个包,导致同一个 Mapper 接口被两个 SqlSessionFactory 重复代理。启动时会报already bound或者Invalid bound statement。

解决办法非常明确:不同数据源的 Mapper 接口必须放在严格隔离的包路径下。包路径不能有父子包含关系,比如com.example.mapper.user和com.example.mapper.userx也不行,因为前缀匹配可能扫到。最好用完全不同的一段路径,比如com.example.mapper.userdb和com.example.mapper.orderdb。

注入的时候,多个 SqlSessionTemplate Bean 存在,不能用@Autowired直接注入,必须用@Resource(name = "userSqlSessionTemplate")或@Qualifier指定。否则也会报歧义。

6.5 连接池维护:Druid监控与连接耗尽检查

多数据源情况下,每个连接池都有自己的最大活跃连接数。有的团队配置了 6 个数据源,每个都默认 8 个连接上限,看起来总连接很多,但某个热点库的流量一打,照样把单库连接池打满。

建议给每个库单独配置连接池上限,不要用一份参数套所有库。Druid 的监控页面要关闭,或者只对必要的库开启。多个 Druid 数据源如果都启动了 Web 监控 Filter,会出现同一个 Servlet 路径被注册多次,启动直接报冲突。常规做法是保留一个库的监控,其余统统关闭,或者把监控信息通过日志方式输出。

连接池耗尽的经典现场是:报错wait millis timeout,但业务没有明显大并发。排查时先看慢 SQL,再看跨库循环查询。有一次我们发现某个接口在 for 循环里调用另一库的 Mapper,单个请求就占几十个连接,并发一上来必然耗尽。

7. 最后分享几点实际体会

多数据源这套技术,本身不难,难点在于边界划分。我把想说的话最后总结成几条实践经验,供参考。

第一,能拆服务就不要只拆数据源。两个库如果业务上确实完全独立,最好的做法是拆成两个微服务,各连各的库,跨库调用走接口。硬塞在一个 Spring Boot 工程里,短期省事,长期维护成本极高。

第二,能静态就不要动态。动态路由确实灵活,也正因为灵活,团队里的人容易在一堆注解里迷失方向,最后不知道哪个方法走了哪个库。静态多 SqlSessionFactory 的包结构本身就承载了库的归属信息,新同事接手时看包就懂。

第三,数据库层面的连接,能少跨就少跨。跨库查询再优化,也绕不开网络延迟。如果一个查询需要频繁跨三个库,产品层面就该考虑做数仓同步或数据冗余,而不是让应用层永远承担这个成本。

我印象最深的一次教训是,一组读写分离接口上线后,监控里出现大量主库读请求。仔细排查才发现,因为 service 方法上加@Transactional,事务拦截器提前获取了主库连接,@DS注解的切换动作根本没机会生效。从那以后,我对“注解 + AOP”这种隐式操作始终保持警惕,能直接把数据源写进配置的,绝不依赖运行时切换。

多数据源的实现方式千变万化,但底层原理就那些:DataSource 的归属,SqlSessionFactory 的绑定,事务连接的边界。把这三条线理清楚,无论项目怎么演进,你都很难被“切库不生效”这种问题坑到。希望这篇手记能帮你少走一点弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询