基于Mybatis实现动态数据源切换

在当今的互联网应用中,微服务大行其道,随着业务的发展和扩展,单一的数据库无法满足日益增长的数据需求,一个业务接口可能需要查询多个数据源的数据组装到一起返回给页面进行呈现,此时就需要考虑使用动态数据源技术。
本文将基于 JDK17 + Spring Boot 3 和 MyBatis 框架实现动态切换数据源功能。

代码开发

  1. 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>

  1. 启动类 Application.java
@MapperScan("com.learning.**.mapper")
@SpringBootApplication(scanBasePackages = {"com.learning"})
public class LearningMybatisApplication {

	public static void main(String[] args) {
		SpringApplication.run(LearningMybatisApplication.class, args);
	}

}
  1. 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";
    }

}
  1. 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();
    }
}
  1. 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>

  1. 配置文件
spring.application.name=learning-mybatis

# ======================== Mybatis ===============
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

动态数据源切换 AOP 实现

  1. 首先,我们需要实现AbstractRoutingDataSource 接口的determineCurrentLookupKey()方法
public class DynamicDataSourceRouter extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSourceType();
    }
}
  1. 创建一个数据源上下文持有者,用于保存和获取当前线程的数据源。
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();
    }

}
  1. 配置你的数据源。这里假设你已经有两个数据源dataSource1dataSource2
@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;
    }


}
  1. 定义注解,用于标记需要切换数据源的方法。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DynamicDataSource {

    String value(); // 数据源名称

}
  1. 定义切面,使用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();

AbstractRoutingDataSourcegetConnection() 方法被调用时,它会调用 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 逻辑,我们可以实现在运行时根据不同的条件动态地选择和切换数据源。这种机制允许应用程序在处理不同的请求或事务时使用不同的数据库连接,从而提供了极大的灵活性和扩展性。

  • 18
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值