准备两个的数据库(不一定是mysql数据库,可能是其他数据库,但是在配置文件中需要加入不同的连接驱动)
-- 数据库1
create database switch_demo_1;
use switch_demo_1;
create table demo(
username varchar(20) character set utf8 not null comment '用户名'
);
insert into demo(username) values('master');
-- 数据库2
create database switch_demo_2;
use switch_demo_2;
create table demo(
username varchar(20) character set utf8 not null comment '用户名'
);
insert into demo(username) values('slave');
<?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.2.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.sin</groupId>
<artifactId>springboot_switch_datasrouce</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot_switch_datasrouce</name>
<description>springboot_switch_datasrouce</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.49</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.16</version>
</dependency>
</dependencies>
</project>
spring:
datasource:
# Druid连接池
type: com.alibaba.druid.pool.DruidDataSource
druid:
# 数据源一
master:
# 数据库驱动类
driverClassName: com.mysql.jdbc.Driver
# 数据库连接URL
jdbcurl: jdbc:mysql://localhost:3306/switch_demo_1
# 数据库用户名
username: root
# 数据库密码
password: 123456
# 数据源二
slave:
# 数据库驱动类
driverClassName: com.mysql.jdbc.Driver
# 数据库连接URL
jdbcurl: jdbc:mysql://localhost:3306/switch_demo_2
# 数据库用户名
username: root
# 数据库密码
password: 123456
# 初始化时创建的连接数,默认为10
initial-size: 10
# 连接池中保持最小空闲连接数,默认为8
min-idle: 8
# 连接池中允许的最大活动连接数,默认为80
max-active: 80
# 当所有连接都在使用中时,等待获取新连接的最长时间(以毫秒为单位),默认为30000毫秒(30秒)。
max-wait: 30000
# 连接池中连接被逐出的时间间隔(以毫秒为单位),默认为60000毫秒(60秒)。
time-between-eviction-runs-millis: 60000
# 连接在池中保持空闲的最长时间(以毫秒为单位),超过这个时间的连接将被逐出,默认为300000毫秒(300秒)。
min-evictable-idle-time-millis: 300000
# 用于验证连接是否有效的SQL查询语句,默认为空字符串。
validation-query: ""
# 是否在空闲连接上执行验证查询,默认为true。
test-while-idle: true
# 是否在借用连接时执行验证查询,默认为false。
test-on-borrow: false
# 是否在归还连接时执行验证查询,默认为false。
test-on-return: false
# 是否使用预处理语句,默认为false。
pool-prepared-statements: false
# 是否使用连接属性,默认为false。
connection-properties: false
package com.sin.pojo;
/**
* @createTime 2023/12/19 14:36
* @createAuthor SIN
* @use
*/
public class TestUser {
private String userName;
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
}
package com.sin.mapper;
import com.sin.pojo.TestUser;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* @createTime 2023/12/19 14:35
* @createAuthor SIN
* @use
*/
@Mapper
public interface TestUserMapper {
@Select("select * from demo")
List<TestUser> selectOne();
}
使用ThreadLocal实现动态切换数据源的原理是通过为每个线程创建一个独立的变量副本来实现的。在Java中,ThreadLocal是一个用于存储线程局部变量的类,它提供了线程安全的方式来访问和修改这些变量。
package com.sin.config;
/**
* @createTime 2023/12/19 14:16
* @createAuthor SIN
* @use 数据源操作
*/
public class DynamicDataSourceContextHolder {
// 通过使用ThreadLocal,在不同线程之间传递数据,而不需要使用全局变量或共享状态
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
/**
* 设置数据源
* @param key 数据源名称
*/
public static void setDataSourceKey(String key) {
contextHolder.set(key);
}
/**
* 获取当前线程的数据源
* @return 数据源名称
*/
public static String getDataSourceKey() {
return contextHolder.get();
}
/**
* 删除当前数据源
*/
public static void clearDataSourceKey() {
contextHolder.remove();
}
}
package com.sin.config;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* @createTime 2023/12/19 14:19
* @createAuthor SIN
* @use
*/
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
// 获取数据源
return DynamicDataSourceContextHolder.getDataSourceKey();
}
}
package com.sin.config;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
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 javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* @createTime 2023/12/19 14:22
* @createAuthor SIN
* @use 设置数据源
*/
@Configuration
public class DynamicDataSourceConfig {
// Bean名称
@Bean(name = "primaryDataSource")
// 读取SpringBoot配置文件中的内容
@ConfigurationProperties(prefix = "spring.datasource.druid.master")
public DataSource primaryDataSource() {
// 使用DataSourceBuilder创建一个新的数据源对象
return DataSourceBuilder.create().build();
}
// Bean名称
@Bean(name = "secondaryDataSource")
// 读取SpringBoot配置文件中的内容
@ConfigurationProperties(prefix = "spring.datasource.druid.slave")
public DataSource secondaryDataSource() {
// 使用DataSourceBuilder创建一个新的数据源对象
return DataSourceBuilder.create().build();
}
// Bean名称
@Bean(name = "dynamicDataSource")
// 当前方法为主数据源的配置方法
@Primary
public DataSource dynamicDataSource(@Qualifier("primaryDataSource") DataSource primaryDataSource,
@Qualifier("secondaryDataSource") DataSource secondaryDataSource) {
// 创建一个Map对象,用于存储目标数据源的名称和对应的数据源对象
Map<Object, Object> targetDataSources = new HashMap<>();
// 将主数据源添加到目标数据源中,并命名为"master"
targetDataSources.put("master", primaryDataSource);
// 将从数据源添加到目标数据源中,并命名为"slave"
targetDataSources.put("slave", secondaryDataSource);
// 创建一个DynamicRoutingDataSource对象,用于实现动态路由功能
DynamicRoutingDataSource dynamicDataSource = new DynamicRoutingDataSource();
// 设置目标数据源映射关系
dynamicDataSource.setTargetDataSources(targetDataSources);
// 设置默认的目标数据源为主数据源
dynamicDataSource.setDefaultTargetDataSource(primaryDataSource);
// 返回创建好的动态数据源对象
return dynamicDataSource;
}
// Bean名称
@Bean(name = "sqlSessionFactory")
// 当前方法为主数据源的配置方法
@Primary
public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dataSource) throws Exception {
// 创建一个SqlSessionFactoryBean对象
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
// 设置数据源
bean.setDataSource(dataSource);
// 返回创建好的SqlSessionFactory对象
return bean.getObject();
}
// Bean
@Bean(name = "transactionManager")
// 当前方法为主数据源的配置方法
@Primary
public DataSourceTransactionManager transactionManager(@Qualifier("dynamicDataSource") DataSource dataSource) {
// 创建一个DataSourceTransactionManager对象,并传入数据源
return new DataSourceTransactionManager(dataSource);
}
}
package com.sin.controller;
import com.sin.config.DynamicDataSourceContextHolder;
import com.sin.mapper.TestUserMapper;
import com.sin.pojo.TestUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* @createTime 2023/12/19 14:29
* @createAuthor SIN
* @use
*/
@RestController
public class TestController {
@Autowired(required = false)
TestUserMapper testUserMapper;
@GetMapping("/getData/{datasourceName}")
public List<TestUser> getMasterData(@PathVariable("datasourceName") String datasourceName){
DynamicDataSourceContextHolder.setDataSourceKey(datasourceName);
List<TestUser> testUsers = testUserMapper.selectOne();
DynamicDataSourceContextHolder.clearDataSourceKey();
return testUsers;
}
}
package com.sin;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
// 排除自动配置的DataSourceAutoConfiguration类,从而避免Spring Boot自动配置数据源
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class SpringbootSwitchDatasrouceApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootSwitchDatasrouceApplication.class, args);
System.out.println("=========================启动成功=======================");
}
}
默认使用master,写入其他数据源识别到并没有aaa,自动走默认数据源
通过在目标方法执行前后添加切点,并在切点中进行数据源的切换和恢复操作。
package com.sin.config;
import java.lang.annotation.*;
/**
* @createTime 2023/12/19 16:38
* @createAuthor SIN
* @use
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DataSource {
String value() default "";
}
package com.sin.config;
/**
* @createTime 2023/12/19 14:16
* @createAuthor SIN
* @use 数据源操作
*/
public class DynamicDataSourceContextHolder {
// 通过使用ThreadLocal,在不同线程之间传递数据,而不需要使用全局变量或共享状态
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
/**
* 设置数据源
* @param key 数据源名称
*/
public static void setDataSourceKey(String key) {
contextHolder.set(key);
}
/**
* 获取当前线程的数据源
* @return 数据源名称
*/
public static String getDataSourceKey() {
return contextHolder.get();
}
/**
* 删除当前数据源
*/
public static void clearDataSourceKey() {
contextHolder.remove();
}
}
package com.sin.config;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* @createTime 2023/12/19 16:39
* @createAuthor SIN
* @use
*/
// 声明这是一个切面类
@Aspect
// 声明这是一个组件,可以被其他组件自动装配
@Component
// 切面的优先级,数值越小优先级越高
@Order(-1)
public class DynamicDataSourceAspect {
/**
* 方法执行前执行的方法,切换数据源
* @param ds
*/
// 定义切点表达式,匹配带有@DataSource注解的方法
@Before("@annotation(ds)")
public void switchDataSource( DataSource ds) {
// 将数据源的值设置到上下文中,以便后续操作使用正确的数据源
DynamicDataSourceContextHolder.setDataSourceKey(ds.value());
}
/**
* 方法执行后执行的方法,恢复数据源
* @param ds
*/
@After("@annotation(ds)")
public void restoreDataSource( DataSource ds) {
// 清除数据源的值,恢复到默认的数据源
DynamicDataSourceContextHolder.clearDataSourceKey();
}
}
package com.sin.config;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSourceKey();
}
}
package com.sin.config;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
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 javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* @createTime 2023/12/19 14:22
* @createAuthor SIN
* @use 设置数据源
*/
@Configuration
public class DynamicDataSourceConfig {
// Bean名称
@Bean(name = "primaryDataSource")
// 读取SpringBoot配置文件中的内容
@ConfigurationProperties(prefix = "spring.datasource.druid.master")
public DataSource primaryDataSource() {
// 使用DataSourceBuilder创建一个新的数据源对象
return DataSourceBuilder.create().build();
}
// Bean名称
@Bean(name = "secondaryDataSource")
// 读取SpringBoot配置文件中的内容
@ConfigurationProperties(prefix = "spring.datasource.druid.slave")
public DataSource secondaryDataSource() {
// 使用DataSourceBuilder创建一个新的数据源对象
return DataSourceBuilder.create().build();
}
// Bean名称
@Bean(name = "dynamicDataSource")
// 当前方法为主数据源的配置方法
@Primary
public DataSource dynamicDataSource(@Qualifier("primaryDataSource") DataSource primaryDataSource,
@Qualifier("secondaryDataSource") DataSource secondaryDataSource) {
// 创建一个Map对象,用于存储目标数据源的名称和对应的数据源对象
Map<Object, Object> targetDataSources = new HashMap<>();
// 将主数据源添加到目标数据源中,并命名为"master"
targetDataSources.put("master", primaryDataSource);
// 将从数据源添加到目标数据源中,并命名为"slave"
targetDataSources.put("slave", secondaryDataSource);
// 创建一个DynamicRoutingDataSource对象,用于实现动态路由功能
DynamicRoutingDataSource dynamicDataSource = new DynamicRoutingDataSource();
// 设置目标数据源映射关系
dynamicDataSource.setTargetDataSources(targetDataSources);
// 设置默认的目标数据源为主数据源
dynamicDataSource.setDefaultTargetDataSource(primaryDataSource);
// 返回创建好的动态数据源对象
return dynamicDataSource;
}
// Bean名称
@Bean(name = "sqlSessionFactory")
// 当前方法为主数据源的配置方法
@Primary
public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dataSource) throws Exception {
// 创建一个SqlSessionFactoryBean对象
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
// 设置数据源
bean.setDataSource(dataSource);
// 返回创建好的SqlSessionFactory对象
return bean.getObject();
}
// Bean
@Bean(name = "transactionManager")
// 当前方法为主数据源的配置方法
@Primary
public DataSourceTransactionManager transactionManager(@Qualifier("dynamicDataSource") DataSource dataSource) {
// 创建一个DataSourceTransactionManager对象,并传入数据源
return new DataSourceTransactionManager(dataSource);
}
}
package com.sin.controller;
import com.sin.config.DataSource;
import com.sin.mapper.TestUserMapper;
import com.sin.pojo.TestUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* @createTime 2023/12/19 16:40
* @createAuthor SIN
* @use
*/
@RestController
public class TestAopController {
@Autowired(required = false)
TestUserMapper testUserMapper;
@GetMapping("/getMasterData")
@DataSource("master")
public List<TestUser> getMasterData(){
List<TestUser> testUsers = testUserMapper.selectOne();
return testUsers;
}
@GetMapping("/getSlaveData")
@DataSource("slave")
public List<TestUser> getSlaveData(){
List<TestUser> testUsers = testUserMapper.selectOne();
return testUsers;
}
}
package com.sin;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
// 排除自动配置的DataSourceAutoConfiguration类,从而避免Spring Boot自动配置数据源
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class SpringbootSwitchDatasrouceApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootSwitchDatasrouceApplication.class, args);
System.out.println("=========================启动成功=======================");
}
}
什么场景下使用原生ThreadLocal来切换数据源?什么场景下使用AOP来切换数据源??
使用原生ThreadLocal来切换数据源的情况:
使用AOP来切换数据源的情况:
在实际开发中,推荐使用原生的ThreadLocal来切换数据源。这是因为ThreadLocal为每个线程提供了独立的变量副本,这样可以避免多线程之间的数据竞争和同步问题。
而AOP虽然可以实现更灵活的数据源切换,但在某些情况下可能会引入额外的复杂性和性能开销。因此,在需要简单、高效且线程安全的数据源切换场景下,使用ThreadLocal是更好的选择。