前面我们用 AOP 实现了操作日志、接口权限校验、接口限流,核心都是「请求增强」场景,不侵入业务代码、优雅解耦。今天我们进入 AOP 另一大经典实战场景------利用 AOP 实现动态多数据源切换,真正做到「业务代码零侵入、注解一键切换主从库/多业务库」。
做过企业级项目的同学都清楚,单数据源根本满足不了中大型项目的需求:随着业务增长,数据量激增,单库的读写压力会越来越大;多业务模块(用户、订单、商品)共用一个数据库,不仅耦合度高,还会出现锁竞争、性能瓶颈;多租户场景下,不同租户的数据需要隔离存储,避免数据泄露。
如果手动在 Service 层来回切换数据源(比如写代码手动切换 Connection),不仅代码冗余、难以维护,还容易出现线程安全问题,一旦切换逻辑出错,就会导致数据查询/写入错误,引发生产事故。
而用「AOP + 自定义注解 + ThreadLocal + AbstractRoutingDataSource」的组合,能完美解决这些问题:只需在方法或类上添加一行注解,就能自动切换到指定数据源,全程不侵入业务代码,切换逻辑统一管理,扩展性极强。
一、核心适用场景
动态多数据源切换不是炫技,而是企业项目的刚需,以下是最常见的4种场景,本篇实战将逐一适配,让你一次学会,终身可用:
-
- 读写分离:主库(master)负责写入操作(新增、修改、删除),从库(slave)负责查询操作,分散数据库读写压力,提升系统性能。比如电商项目中,下单、支付走主库,商品列表查询、订单历史查询走从库。
-
- 多业务库隔离:大型项目中,将不同业务模块的数据库分离,比如用户库(db_user)、订单库(db_order)、商品库(db_goods),降低模块间耦合,避免单库故障影响全系统,同时便于单独维护和扩容。
-
- 多租户架构:SaaS 系统中,不同租户的数据存储在不同的数据库(或不同 Schema),通过租户ID动态切换数据源,实现数据隔离,保障租户数据安全,比如企业管理系统、CRM系统。
-
- 历史库/实时库分离:核心业务走实时库(存储近期数据,性能优先),历史数据归档到历史库(存储远期数据,容量优先),查询历史数据时切换到历史库,避免历史数据查询影响实时业务性能。
补充说明:本篇实战以「读写分离(1主2从)」为基础,同时提供多业务库、多租户的扩展方案,代码可灵活适配不同场景,无需大量修改。
二、整体架构思路
动态数据源切换的核心是「路由」------根据注解标记,将当前请求路由到指定的数据源。整体架构基于 Spring 提供的 AbstractRoutingDataSource 类,结合 AOP 和 ThreadLocal 实现,步骤清晰、逻辑连贯,具体流程如下:
-
- 配置多数据源:在 application.yml 中配置多个数据源(主库、从库、业务库等),指定每个数据源的 URL、用户名、密码、驱动类。
-
- 实现动态数据源路由:继承 Spring 提供的 AbstractRoutingDataSource 类,重写 determineCurrentLookupKey 方法,该方法的返回值就是当前要使用的数据源标识(如 master、slave1)。
-
- 线程安全存储数据源标识:用 ThreadLocal 保存当前线程要使用的数据源标识,避免多线程环境下数据源错乱(ThreadLocal 是线程隔离的,每个线程有独立的存储空间)。
-
- 自定义切换注解:创建 @DS 注解,用于标记类或方法需要使用的数据源,注解值为数据源标识(如 @DS("slave1"))。
-
- AOP 切面拦截处理:创建 AOP 切面,拦截所有添加了 @DS 注解的类或方法,在方法执行前,从注解中获取数据源标识,存入 ThreadLocal;方法执行完毕后,清空 ThreadLocal,避免线程复用导致的数据源污染。
-
- 配置数据源Bean:将所有数据源注入 Spring 容器,通过 DynamicDataSource 整合所有数据源,设置默认数据源(如主库),并将其作为 Spring 的主数据源。
-
- 测试与优化:覆盖读写分离、多库切换等场景,测试数据源切换是否正常,同时处理事务兼容、线程安全等问题,优化切换性能。
核心原理:AbstractRoutingDataSource 会在获取数据库连接时,调用 determineCurrentLookupKey 方法获取当前数据源标识,然后从配置的数据源集合中找到对应的数据源,实现动态路由。
三、完整代码
本次实战基于 SpringBoot 2.7.x 版本,使用 MySQL 数据库、HikariCP 连接池(性能最优的连接池之一),全程无复杂依赖,所有代码都经过企业项目验证,可直接复制到项目中,只需修改数据源配置和包路径,就能快速落地。
步骤1:导入核心依赖(pom.xml)
需要导入 SpringBoot 核心依赖、AOP 依赖、JDBC 依赖、MySQL 驱动、连接池依赖,无需额外导入其他包,pom.xml 如下:
go
<!-- 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 如下:
go
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 保存当前线程的数据源标识,确保多线程环境下数据源不错乱,同时提供设置、获取、清空数据源标识的方法,必须在方法执行完毕后清空,避免线程复用污染。
go
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 方法,该方法会在获取数据库连接时被调用,返回当前要使用的数据源标识,从而实现数据源动态路由。
go
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 能正确识别数据源。
go
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),支持类级别和方法级别注解,方法级别注解优先级高于类级别注解(灵活适配不同场景)。
go
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)),保证切面在事务之前执行,否则数据源切换会失效。
go
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 数据源。
go
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"),实现读写分离。
go
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 主库),适合写入操作或不需要切换数据源的场景。
go
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 单元测试
go
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 测试结果验证
运行单元测试,查看控制台日志,若出现以下日志,说明数据源切换成功:
go
当前线程[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 实战干货,下期再见❤️