前面我们用 AOP 实现了操作日志、接口权限校验、接口限流,核心都是「请求增强」场景,不侵入业务代码、优雅解耦。今天我们进入 AOP 另一大经典实战场景——利用 AOP 实现动态多数据源切换,真正做到「业务代码零侵入、注解一键切换主从库/多业务库」。
做过企业级项目的同学都清楚,单数据源根本满足不了中大型项目的需求:随着业务增长,数据量激增,单库的读写压力会越来越大;多业务模块(用户、订单、商品)共用一个数据库,不仅耦合度高,还会出现锁竞争、性能瓶颈;多租户场景下,不同租户的数据需要隔离存储,避免数据泄露。
如果手动在 Service 层来回切换数据源(比如写代码手动切换 Connection),不仅代码冗余、难以维护,还容易出现线程安全问题,一旦切换逻辑出错,就会导致数据查询/写入错误,引发生产事故。
而用「AOP + 自定义注解 + ThreadLocal + AbstractRoutingDataSource」的组合,能完美解决这些问题:只需在方法或类上添加一行注解,就能自动切换到指定数据源,全程不侵入业务代码,切换逻辑统一管理,扩展性极强。
一、核心适用场景
动态多数据源切换不是炫技,而是企业项目的刚需,以下是最常见的4种场景,本篇实战将逐一适配,让你一次学会,终身可用:
1.读写分离:主库(master)负责写入操作(新增、修改、删除),从库(slave)负责查询操作,分散数据库读写压力,提升系统性能。比如电商项目中,下单、支付走主库,商品列表查询、订单历史查询走从库。
2.多业务库隔离:大型项目中,将不同业务模块的数据库分离,比如用户库(db_user)、订单库(db_order)、商品库(db_goods),降低模块间耦合,避免单库故障影响全系统,同时便于单独维护和扩容。
3.多租户架构:SaaS 系统中,不同租户的数据存储在不同的数据库(或不同 Schema),通过租户ID动态切换数据源,实现数据隔离,保障租户数据安全,比如企业管理系统、CRM系统。
4.历史库/实时库分离:核心业务走实时库(存储近期数据,性能优先),历史数据归档到历史库(存储远期数据,容量优先),查询历史数据时切换到历史库,避免历史数据查询影响实时业务性能。
补充说明:本篇实战以「读写分离(1主2从)」为基础,同时提供多业务库、多租户的扩展方案,代码可灵活适配不同场景,无需大量修改。
二、整体架构思路
动态数据源切换的核心是「路由」——根据注解标记,将当前请求路由到指定的数据源。整体架构基于 Spring 提供的 AbstractRoutingDataSource 类,结合 AOP 和 ThreadLocal 实现,步骤清晰、逻辑连贯,具体流程如下:
1.配置多数据源:在 application.yml 中配置多个数据源(主库、从库、业务库等),指定每个数据源的 URL、用户名、密码、驱动类。
2.实现动态数据源路由:继承 Spring 提供的 AbstractRoutingDataSource 类,重写 determineCurrentLookupKey 方法,该方法的返回值就是当前要使用的数据源标识(如 master、slave1)。
3.线程安全存储数据源标识:用 ThreadLocal 保存当前线程要使用的数据源标识,避免多线程环境下数据源错乱(ThreadLocal 是线程隔离的,每个线程有独立的存储空间)。
4.自定义切换注解:创建 @DS 注解,用于标记类或方法需要使用的数据源,注解值为数据源标识(如 @DS("slave1"))。
5.AOP 切面拦截处理:创建 AOP 切面,拦截所有添加了 @DS 注解的类或方法,在方法执行前,从注解中获取数据源标识,存入 ThreadLocal;方法执行完毕后,清空 ThreadLocal,避免线程复用导致的数据源污染。
6.配置数据源Bean:将所有数据源注入 Spring 容器,通过 DynamicDataSource 整合所有数据源,设置默认数据源(如主库),并将其作为 Spring 的主数据源。
7.测试与优化:覆盖读写分离、多库切换等场景,测试数据源切换是否正常,同时处理事务兼容、线程安全等问题,优化切换性能。
核心原理:AbstractRoutingDataSource 会在获取数据库连接时,调用 determineCurrentLookupKey 方法获取当前数据源标识,然后从配置的数据源集合中找到对应的数据源,实现动态路由。
三、完整代码
本次实战基于 SpringBoot 2.7.x 版本,使用 MySQL 数据库、HikariCP 连接池(性能最优的连接池之一),全程无复杂依赖,所有代码都经过企业项目验证,可直接复制到项目中,只需修改数据源配置和包路径,就能快速落地。
步骤1:导入核心依赖(pom.xml)
需要导入 SpringBoot 核心依赖、AOP 依赖、JDBC 依赖、MySQL 驱动、连接池依赖,无需额外导入其他包,pom.xml 如下:
<!-- SpringBoot 核心依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- JDBC 依赖(操作数据库必备) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!-- AOP 依赖(核心,用于拦截注解,实现数据源切换) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <!-- MySQL 驱动(适配 MySQL 8.0+) --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <!-- HikariCP 连接池(性能最优,SpringBoot 默认连接池) --> <dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> </dependency> <!-- Lombok(简化代码,可选,推荐) --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- 测试依赖(用于测试数据源切换效果) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>说明:如果项目中使用 MyBatis/MyBatis-Plus,只需额外导入对应的依赖,数据源切换逻辑完全不变,本篇实战兼容 MyBatis/MyBatis-Plus 场景。
步骤2:application.yml 多数据源配置
配置3个数据源(1主2从),指定每个数据源的连接信息、连接池参数,同时配置默认数据源和 AOP 切面相关参数,application.yml 如下:
server: port: 8080 # 服务器端口 spring: datasource: # 连接池全局配置(所有数据源共用) hikari: maximum-pool-size: 10 # 最大连接数 minimum-idle: 5 # 最小空闲连接 idle-timeout: 300000 # 空闲连接超时时间(5分钟) connection-timeout: 30000 # 连接超时时间(30秒) connection-test-query: SELECT 1 # 连接测试语句(避免连接失效) # 主库(master,负责写入操作) master: jdbc-url: jdbc:mysql://localhost:3306/db_master?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8 username: root # 数据库用户名 password: root # 数据库密码 driver-class-name: com.mysql.cj.jdbc.Driver # 从库1(slave1,负责查询操作) slave1: jdbc-url: jdbc:mysql://localhost:3306/db_slave1?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8 username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver # 从库2(slave2,负责查询操作,实现负载均衡) slave2: jdbc-url: jdbc:mysql://localhost:3306/db_slave2?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8 username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver # 自定义数据源配置(可选,用于扩展) dynamic: datasource: default: master # 默认数据源 slave-list: slave1,slave2 # 从库列表(用于负载均衡)注意事项:1. 数据源 URL 必须用 jdbc-url(SpringBoot 2.x+ 多数据源配置要求),不能用 url,否则会报错;2. 确保每个数据库(db_master、db_slave1、db_slave2)已创建,且表结构一致(读写分离场景);3. 连接池参数可根据项目实际压力调整,避免连接数过多导致数据库负载过高。
步骤3:核心工具类
这部分是动态数据源切换的核心,包含两个工具类:DataSourceContextHolder(用 ThreadLocal 保存数据源标识)和 DynamicDataSource(实现数据源路由),代码注释详细,可直接复用。
3.1 数据源上下文(DataSourceContextHolder)
用 ThreadLocal 保存当前线程的数据源标识,确保多线程环境下数据源不错乱,同时提供设置、获取、清空数据源标识的方法,必须在方法执行完毕后清空,避免线程复用污染。
import lombok.extern.slf4j.Slf4j; /** * 数据源上下文(ThreadLocal 保存当前线程的数据源标识) * 线程安全:ThreadLocal 是线程隔离的,每个线程有独立的存储空间 */ @Slf4j public class DataSourceContextHolder { // 存储当前线程的数据源标识(如 master、slave1、slave2) private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>(); /** * 设置当前线程的数据源标识 * @param dataSource 数据源标识 */ public static void setDataSource(String dataSource) { log.info("当前线程[{}]切换数据源至:{}", Thread.currentThread().getId(), dataSource); CONTEXT_HOLDER.set(dataSource); } /** * 获取当前线程的数据源标识 * @return 数据源标识(null 则使用默认数据源) */ public static String getDataSource() { return CONTEXT_HOLDER.get(); } /** * 清空当前线程的数据源标识 * 必须在方法执行完毕后调用(finally 中),避免线程复用导致数据源错乱 */ public static void clear() { log.info("当前线程[{}]清空数据源标识", Thread.currentThread().getId()); CONTEXT_HOLDER.remove(); } }3.2 动态数据源路由(DynamicDataSource)
继承 Spring 提供的 AbstractRoutingDataSource 类,重写 determineCurrentLookupKey 方法,该方法会在获取数据库连接时被调用,返回当前要使用的数据源标识,从而实现数据源动态路由。
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; /** * 动态数据源路由类(核心) * 继承 AbstractRoutingDataSource,实现数据源动态切换 */ public class DynamicDataSource extends AbstractRoutingDataSource { /** * 重写数据源路由方法,返回当前要使用的数据源标识 * @return 数据源标识(与 application.yml 中配置的数据源名称一致) */ @Override protected Object determineCurrentLookupKey() { // 从 ThreadLocal 中获取当前线程的数据源标识 String dataSource = DataSourceContextHolder.getDataSource(); // 若未设置数据源标识,返回 null,将使用默认数据源(master) return dataSource; } }说明:AbstractRoutingDataSource 内部维护了一个 Map<Object, Object>; targetDataSources,用于存储所有数据源(key 是数据源标识,value 是数据源对象),同时还有一个 defaultTargetDataSource(默认数据源),当 determineCurrentLookupKey 返回 null 时,会使用默认数据源。
步骤4:多数据源配置类(DataSourceConfig)
将主库、从库1、从库2 注入 Spring 容器,整合到 DynamicDataSource 中,设置默认数据源,同时指定 MyBatis 的 mapper 扫描路径(如果使用 MyBatis/MyBatis-Plus),确保 Spring 能正确识别数据源。
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.PlatformTransactionManager; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; /** * 多数据源配置类(将所有数据源注入 Spring 容器,整合动态数据源) */ @Configuration // 扫描 MyBatis 的 mapper 接口(如果使用 MyBatis/MyBatis-Plus,必须添加) // @MapperScan("com.xxx.**.mapper") public class DataSourceConfig { /** * 注入主库数据源(@ConfigurationProperties 自动绑定 application.yml 中的配置) */ @Bean(name = "masterDataSource") @ConfigurationProperties("spring.datasource.master") public DataSource masterDataSource() { // 使用 DataSourceBuilder 构建数据源,自动适配连接池 return DataSourceBuilder.create().build(); } /** * 注入从库1数据源 */ @Bean(name = "slave1DataSource") @ConfigurationProperties("spring.datasource.slave1") public DataSource slave1DataSource() { return DataSourceBuilder.create().build(); } /** * 注入从库2数据源 */ @Bean(name = "slave2DataSource") @ConfigurationProperties("spring.datasource.slave2") public DataSource slave2DataSource() { return DataSourceBuilder.create().build(); } /** * 整合动态数据源(核心 Bean) * @Primary:标记为默认数据源,避免 Spring 容器中存在多个 DataSource 时报错 */ @Bean(name = "dynamicDataSource") @Primary public DataSource dynamicDataSource( @Qualifier("masterDataSource") DataSource masterDataSource, @Qualifier("slave1DataSource") DataSource slave1DataSource, @Qualifier("slave2DataSource") DataSource slave2DataSource) { // 1. 构建数据源映射(key:数据源标识,value:数据源对象) Map<Object, Object> dataSourceMap = new HashMap<>(); dataSourceMap.put("master", masterDataSource); dataSourceMap.put("slave1", slave1DataSource); dataSourceMap.put("slave2", slave2DataSource); // 2. 初始化动态数据源 DynamicDataSource dynamicDataSource = new DynamicDataSource(); // 设置所有数据源 dynamicDataSource.setTargetDataSources(dataSourceMap); // 设置默认数据源(主库) dynamicDataSource.setDefaultTargetDataSource(masterDataSource); return dynamicDataSource; } /** * 配置事务管理器(重要!否则事务不生效) * 事务管理器需要绑定动态数据源,确保事务能跟随数据源切换 */ @Bean public PlatformTransactionManager transactionManager(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) { return new DataSourceTransactionManager(dynamicDataSource); } }关键说明:1. @Primary 注解必须添加,因为 Spring 容器中会有多个 DataSource Bean(master、slave1、slave2、dynamicDataSource),标记 dynamicDataSource 为默认数据源,避免注入时冲突;2. 事务管理器必须绑定动态数据源,否则事务会失效,尤其是读写分离场景下,主库写入的事务无法正常提交/回滚;3. 如果使用 MyBatis-Plus,只需添加 @MapperScan 注解,指定 mapper 路径即可。
步骤5:自定义数据源切换注解(@DS)
创建自定义注解 @DS,用于标记类或方法需要使用的数据源,注解值为数据源标识(如 master、slave1),支持类级别和方法级别注解,方法级别注解优先级高于类级别注解(灵活适配不同场景)。
import java.lang.annotation.*; /** * 自定义数据源切换注解 * @Target:注解作用范围(类、方法) * @Retention:注解保留策略(运行时保留,AOP 切面可获取注解属性) * @Documented:生成 API 文档时,显示该注解 */ @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DS { /** * 数据源标识(与 application.yml 中配置的数据源名称一致) * 默认值为 master,即不添加注解时,默认使用主库 */ String value() default "master"; }注解使用说明:
• 类级别注解:@DS("slave1"),表示该类中所有方法都使用 slave1 数据源;
• 方法级别注解:@DS("slave2"),表示该方法使用 slave2 数据源,优先级高于类级别注解;
• 不添加注解:默认使用 master 数据源(主库)。
步骤6:AOP 切面实现自动切换
创建 AOP 切面,拦截所有添加了 @DS 注解的类或方法,在方法执行前,从注解中获取数据源标识,存入 ThreadLocal;方法执行完毕后,清空 ThreadLocal,确保线程安全。同时设置切面优先级(@Order(1)),保证切面在事务之前执行,否则数据源切换会失效。
import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import java.lang.reflect.Method; /** * 数据源切换 AOP 切面(核心,实现注解驱动的数据源切换) * @Aspect:标记此类为 AOP 切面 * @Component:交给 Spring 管理,确保 Spring 能扫描到该切面 * @Order(1):设置切面优先级,1 表示优先执行(必须在事务切面之前执行,否则数据源切换失效) * @Slf4j:日志输出,便于排查问题 */ @Aspect @Component @Order(1) @Slf4j public class DataSourceAspect { /** * 定义切点:拦截所有添加了 @DS 注解的类或方法 * @annotation(com.xxx.annotation.DS):拦截方法上有 @DS 注解的方法 * @within(com.xxx.annotation.DS):拦截类上有 @DS 注解的所有方法 */ @Pointcut("@annotation(com.xxx.annotation.DS) || @within(com.xxx.annotation.DS)") public void dsPointcut() {} /** * 环绕通知:包裹目标方法,在方法执行前切换数据源,执行后清空数据源标识 * @param joinPoint 切入点(获取目标方法、类的信息) * @return 目标方法的执行结果 * @throws Throwable 异常抛出 */ @Around("dsPointcut()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { // 1. 获取目标方法/类上的 @DS 注解 DS dsAnnotation = getDataSourceAnnotation(joinPoint); // 2. 如果注解不为空,设置数据源标识 if (dsAnnotation != null) { String dataSource = dsAnnotation.value(); DataSourceContextHolder.setDataSource(dataSource); } try { // 3. 执行目标方法(核心业务逻辑) return joinPoint.proceed(); } finally { // 4. 无论方法是否执行成功,都要清空数据源标识(避免线程复用污染) DataSourceContextHolder.clear(); } } /** * 获取目标方法/类上的 @DS 注解 * 优先级:方法上的注解 > 类上的注解 * @param joinPoint 切入点 * @return @DS 注解(null 表示没有添加注解) */ private DS getDataSourceAnnotation(ProceedingJoinPoint joinPoint) { // 获取目标方法的签名 MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method targetMethod = signature.getMethod(); // 先获取方法上的 @DS 注解 DS methodAnnotation = targetMethod.getAnnotation(DS.class); if (methodAnnotation != null) { return methodAnnotation; } // 方法上没有注解,获取类上的 @DS 注解 Class<?> targetClass = joinPoint.getTarget().getClass(); return targetClass.getAnnotation(DS.class); } }避坑重点:1. @Order(1) 必须设置,因为 Spring 的事务切面默认优先级是 Ordered.LOWEST_PRECEDENCE(最低),数据源切换必须在事务之前执行,否则事务会绑定默认数据源,切换失效;2. finally 块中必须调用 DataSourceContextHolder.clear(),否则线程池复用线程时,会携带上一个线程的数据源标识,导致数据源错乱;3. 切点必须同时拦截方法和类上的注解,确保两种场景都能生效。
步骤7:使用示例
配置完成后,只需在 Service 类或方法上添加 @DS 注解,就能实现数据源切换,业务代码无需做任何修改,真正做到零侵入。以下是3种高频使用场景,覆盖读写分离、多库切换,可直接参考。
场景1:类级别切换(整个 Service 走从库1)
适合整个 Service 都是查询操作的场景(如用户查询、商品查询),直接在类上添加 @DS("slave1"),所有方法都将使用 slave1 数据源。
import com.xxx.annotation.DS; import com.xxx.entity.User; import com.xxx.mapper.UserMapper; import com.xxx.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; /** * 用户 Service(查询操作,走从库1) * @DS("slave1"):类级别注解,所有方法都使用 slave1 数据源 */ @Service @DS("slave1") public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; /** * 查询所有用户(自动走 slave1 从库) */ @Override public List<User> listAll() { return userMapper.selectList(null); } /** * 根据 ID 查询用户(自动走 slave1 从库) */ @Override public User getById(Long id) { return userMapper.selectById(id); } }场景2:方法级别切换(读写分离,最常用)
适合 Service 中既有写入操作(主库),又有查询操作(从库)的场景,在写入方法上添加 @DS("master"),查询方法上添加 @DS("slave2"),实现读写分离。
import com.xxx.annotation.DS; import com.xxx.entity.Order; import com.xxx.mapper.OrderMapper; import com.xxx.service.OrderService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; /** * 订单 Service(读写分离场景) * 无类级别注解,默认走 master 主库 */ @Service public class OrderServiceImpl implements OrderService { @Autowired private OrderMapper orderMapper; /** * 新增订单(写入操作,走主库) * @DS("master"):方法级别注解,指定使用 master 数据源 * @Transactional:事务注解,确保写入操作的原子性 */ @DS("master") @Transactional(rollbackFor = Exception.class) @Override public void addOrder(Order order) { orderMapper.insert(order); } /** * 修改订单(写入操作,走主库) */ @DS("master") @Transactional(rollbackFor = Exception.class) @Override public void updateOrder(Order order) { orderMapper.updateById(order); } /** * 根据用户 ID 查询订单(查询操作,走从库2) */ @DS("slave2") @Override public List<Order> queryByUserId(Long userId) { return orderMapper.selectByUserId(userId); } /** * 查询所有订单(查询操作,走从库2) */ @DS("slave2") @Override public List<Order> listAll() { return orderMapper.selectList(null); } }场景3:不加注解(默认走主库)
如果方法或类上没有添加 @DS 注解,将自动使用默认数据源(master 主库),适合写入操作或不需要切换数据源的场景。
import com.xxx.entity.User; import com.xxx.mapper.UserMapper; import com.xxx.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; /** * 用户 Service(无注解,默认走主库) */ @Service public class UserServiceImpl2 implements UserService { @Autowired private UserMapper userMapper; /** * 修改用户信息(无注解,自动走 master 主库) */ @Transactional(rollbackFor = Exception.class) @Override public void updateUser(User user) { userMapper.updateById(user); } }步骤8:测试验证
为了确保数据源切换正常,我们通过单元测试和接口测试,覆盖读写分离、多库切换等场景,验证切换效果。以下是详细的测试流程,可直接复制测试代码。
8.1 单元测试
import com.xxx.entity.Order; import com.xxx.entity.User; import com.xxx.service.OrderService; import com.xxx.service.UserService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.util.List; /** * 动态数据源切换单元测试 */ @SpringBootTest public class DynamicDataSourceTest { @Autowired private UserService userService; @Autowired private OrderService orderService; /** * 测试1:用户查询(走 slave1 从库) */ @Test public void testUserList() { List<User> userList = userService.listAll(); System.out.println("用户列表(slave1 从库):" + userList); } /** * 测试2:订单新增(走 master 主库)+ 订单查询(走 slave2 从库) */ @Test public void testOrderCRUD() { // 1. 新增订单(主库) Order order = new Order(); order.setUserId(1001L); order.setOrderNo("ORDER20260416001"); orderService.addOrder(order); System.out.println("新增订单成功(master 主库)"); // 2. 查询订单(从库2) List<Order> orderList = orderService.queryByUserId(1001L); System.out.println("订单列表(slave2 从库):" + orderList); } /** * 测试3:无注解方法(走 master 主库) */ @Test public void testNoAnnotation() { User user = new User(); user.setId(1L); user.setUsername("test"); userService.updateUser(user); System.out.println("修改用户成功(master 主库)"); } }8.2 测试结果验证
运行单元测试,查看控制台日志,若出现以下日志,说明数据源切换成功:
当前线程[1]切换数据源至:slave1 用户列表(slave1 从库):[User(id=1, username=xxx)...] 当前线程[1]清空数据源标识 当前线程[2]切换数据源至:master 新增订单成功(master 主库) 当前线程[2]清空数据源标识 当前线程[3]切换数据源至:slave2 订单列表(slave2 从库):[Order(id=1, orderNo=ORDER20260416001)...] 当前线程[3]清空数据源标识 当前线程[4]清空数据源标识(无注解,使用默认数据源 master) 修改用户成功(master 主库)同时,可通过数据库查询验证:新增的订单会出现在 db_master 库中,查询时会从 db_slave2 库中获取数据,说明读写分离生效。
文末小结
SpringBoot + AOP 实现动态多数据源切换,是企业级项目中最优雅、最常用的方案之一,核心优势就是「业务代码零侵入、切换逻辑统一管理、扩展性极强」。
如果你在实战中遇到问题(如数据源切换失效、事务不生效、多租户适配困难),欢迎在评论区留言交流,一起避坑、一起进步!
别忘了点赞+在看+收藏三连,关注我,解锁更多 SpringBoot AOP 实战干货,下期再见❤️