基于Mybatis实现动态数据源切换在当今的互联网应用中,微服务大行其道,随着业务的发展和扩展,单一的数据库无法满足日益
在当今的互联网应用中,微服务大行其道,随着业务的发展和扩展,单一的数据库无法满足日益增长的数据需求,一个业务接口可能需要查询多个数据源的数据组装到一起返回给页面进行呈现,此时就需要考虑使用动态数据源技术。 本文将基于 JDK17 + Spring Boot 3 和 MyBatis 框架实现动态切换数据源功能。
代码开发
- pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.learning</groupId>
<artifactId>learning-mybatis</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>learning-mybatis</name>
<description>learning-mybatis</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<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>3.0.3</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<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>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter-test</artifactId>
<version>3.0.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
- 启动类 Application.java
@MapperScan("com.learning.**.mapper")
@SpringBootApplication(scanBasePackages = {"com.learning"})
public class LearningMybatisApplication {
public static void main(String[] args) {
SpringApplication.run(LearningMybatisApplication.class, args);
}
}
- Controller
@RestController
@RequestMapping("/student")
public class StudentController {
@Autowired
StudentService studentService;
@GetMapping("/selectAll")
public String selectAll() {
List<Student> students = studentService.selectAll();
for (Student student : students) {
System.out.println(student);
}
return "success";
}
}
- Service 和 Impl
public interface IStudentService {
List<Student> selectAll();
}
@Service
public class StudentServiceImpl implements IStudentService {
@Autowired
StudentMapper mapper;
@Override
public List<Student> selectAll() {
List<Student> students = mapper.selectAll();
List<Student> dataSource2All = mapper.selectDataSource2All();
return mapper.selectAll();
}
}
- Mapper
public interface StudentMapper {
@DynamicDataSource("dataSource1")
List<Student> selectAll();
@DynamicDataSource("dataSource2")
List<Student> selectDataSource2All();
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.learning.mybatis.mapper.StudentMapper">
<resultMap id="BaseResultMap" type="com.learning.mybatis.entities.Student">
<id property="id" column="id" jdbcType="VARCHAR"/>
<result property="name" column="name" jdbcType="VARCHAR"/>
<result property="age" column="age" jdbcType="INTEGER"/>
</resultMap>
<sql id="Base_Column_List">
id,name,age
</sql>
<select id="selectAll" resultMap="BaseResultMap">
select
id,name,age
from student
</select>
<select id="selectDataSource2All" resultMap="BaseResultMap">
select
id,name
from tx
</select>
</mapper>
- 配置文件
spring.application.name=learning-mybatis
# ======================== Mybatis ===============
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
动态数据源切换 AOP 实现
- 首先,我们需要实现
AbstractRoutingDataSource
接口的determineCurrentLookupKey()方法
public class DynamicDataSourceRouter extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSourceType();
}
}
- 创建一个数据源上下文持有者,用于保存和获取当前线程的数据源。
public class DynamicDataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new InheritableThreadLocal<>();
public static void setDataSourceType(String dataSourceType) {
contextHolder.set(dataSourceType);
}
public static String getDataSourceType() {
return contextHolder.get();
}
public static void clearDataSourceType() {
contextHolder.remove();
}
}
- 配置你的数据源。这里假设你已经有两个数据源
dataSource1
和dataSource2
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
DynamicDataSourceRouter routingDataSource = new DynamicDataSourceRouter();
Map<Object, Object> dataSourceMap = new HashMap<>();
dataSourceMap.put("dataSource1", dataSource1()); // 你的第一个数据源
dataSourceMap.put("dataSource2", dataSource2()); // 你的第二个数据源
routingDataSource.setTargetDataSources(dataSourceMap);
routingDataSource.setDefaultTargetDataSource(dataSource1()); // 默认数据源
return routingDataSource;
}
private DataSource dataSource1() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/db2024?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true");
dataSource.setUsername("root");
dataSource.setPassword("root");
// 设置其他HikariDataSource的属性,如连接池大小等
return dataSource;
}
private DataSource dataSource2() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/tx2021?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true");
dataSource.setUsername("root");
dataSource.setPassword("root");
// 设置其他HikariDataSource的属性,如连接池大小等
return dataSource;
}
}
- 定义注解,用于标记需要切换数据源的方法。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DynamicDataSource {
String value(); // 数据源名称
}
- 定义切面,使用AOP来拦截带有
@DynamicDataSource
注解的方法,并在方法执行前后切换数据源
@Aspect
@Order(-1) // 确保该AOP在事务AOP之前执行
@Component
public class DynamicDataSourceAspect {
@Before("@annotation(dynamicDataSource)")
public void switchDataSource(JoinPoint point, DynamicDataSource dynamicDataSource) {
DynamicDataSourceContextHolder.setDataSourceType(dynamicDataSource.value());
}
@After("@annotation(dynamicDataSource)")
public void restoreDataSource(JoinPoint point, DynamicDataSource dynamicDataSource) {
DynamicDataSourceContextHolder.clearDataSourceType();
}
}
至此,完活,拿去测试看效果。
动态切换数据源实现原理分析
核心代码:AbstractRoutingDataSource
AbstractRoutingDataSource
是 Spring 框架提供的一个抽象类,它实现了 DataSource
接口,内部维护了一个用来存储数据源和它们对应的 key的Map,这个 Map 是在构造函数或者配置方法(如 setTargetDataSources
)中设置的。
// org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource
@Nullable
private Map<Object, Object> targetDataSources;
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
this.targetDataSources = targetDataSources;
}
determineCurrentLookupKey()
方法是 AbstractRoutingDataSource
的核心。它是一个抽象方法,子类必须实现它来提供当前的 key。
// org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource
@Nullable
protected abstract Object determineCurrentLookupKey();
当 AbstractRoutingDataSource
的 getConnection()
方法被调用时,它会调用 determineCurrentLookupKey()
来获取当前的数据源 key,然后使用这个 key 从 Map 中获取对应的数据源。
// org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource
public Connection getConnection() throws SQLException {
return this.determineTargetDataSource().getConnection();
}
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = this.determineCurrentLookupKey();
DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
} else {
return dataSource;
}
}
一旦 determineTargetDataSource()
方法返回了合适的数据源,AbstractRoutingDataSource
就会使用这个数据源来获取数据库连接。
由于 determineCurrentLookupKey()
方法在每个数据库操作之前都会被调用,所以只要在适当的地方修改 determineCurrentLookupKey()
的实现,就可以实现在不同的数据库操作间切换数据源。
总结
通过实现 AbstractRoutingDataSource.determineCurrentLookupKey()
方法,并结合 Spring 框架内部的 AbstractRoutingDataSource
逻辑,我们可以实现在运行时根据不同的条件动态地选择和切换数据源。这种机制允许应用程序在处理不同的请求或事务时使用不同的数据库连接,从而提供了极大的灵活性和扩展性。
转载自:https://juejin.cn/post/7415913959595638820