SpringBoot动态切换数据源

发布时间:2023年12月20日

SpringBoot动态切换数据源

数据库

准备两个的数据库(不一定是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

TestUser.java

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;
    }
}

TestUserMapper.java

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)

使用ThreadLocal实现动态切换数据源的原理是通过为每个线程创建一个独立的变量副本来实现的。在Java中,ThreadLocal是一个用于存储线程局部变量的类,它提供了线程安全的方式来访问和修改这些变量。

DynamicDataSourceContextHolder.java

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

DynamicRoutingDataSource.java

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

DynamicDataSourceConfig.java

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);
    }
}

TestController.java

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;
    }

}

SpringbootSwitchDatasrouceApplication.java

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,自动走默认数据源

在这里插入图片描述

方式二(面向切面)

通过在目标方法执行前后添加切点,并在切点中进行数据源的切换和恢复操作。

DataSource.java

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

DynamicDataSourceContextHolder.java

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

DynamicDataSourceAspect.java

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

DynamicRoutingDataSource.java

package com.sin.config;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class DynamicRoutingDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSourceKey();
    }
}

DynamicDataSourceConfig.java

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);
    }
}

TestAopController.java

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;
    }
}

SpringbootSwitchDatasrouceApplication.java

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来切换数据源的情况:

  • 当需要为每个线程独立存储和管理数据源时,可以使用ThreadLocal。每个线程都可以访问到自己的数据源副本,避免了多线程之间的数据竞争和同步问题。
  • 当数据源的切换逻辑比较简单,只需要在方法级别进行切换时,可以使用ThreadLocal。ThreadLocal的使用相对简单,只需在需要切换数据源的方法中获取或设置对应的数据源即可。
  • 当性能要求较高时,可以考虑使用ThreadLocal。由于ThreadLocal是线程本地存储,每个线程都有自己的变量副本,不需要进行额外的同步操作,因此性能较好。

使用AOP来切换数据源的情况:

  • 当需要在方法执行前后进行一些通用的操作,如权限验证、日志记录等,并且需要在这些操作中进行数据源的切换时,可以使用AOP。通过定义切点和通知,可以在方法执行前获取正确的数据源,并在方法执行后恢复原始的数据源。
  • 当需要对多个方法进行统一的权限控制和数据源切换时,可以使用AOP。通过将切点设置为匹配多个方法的模式,可以将这些方法都纳入切面的逻辑中,实现统一的数据源切换和权限控制。
  • 当需要进行更复杂的数据源切换逻辑时,可以使用AOP。AOP提供了强大的拦截器和环绕通知等功能,可以实现更灵活的数据源切换逻辑,如根据请求参数动态选择数据源等。

在实际开发中,推荐使用原生的ThreadLocal来切换数据源。这是因为ThreadLocal为每个线程提供了独立的变量副本,这样可以避免多线程之间的数据竞争和同步问题。

而AOP虽然可以实现更灵活的数据源切换,但在某些情况下可能会引入额外的复杂性和性能开销。因此,在需要简单、高效且线程安全的数据源切换场景下,使用ThreadLocal是更好的选择。

文章来源:https://blog.csdn.net/qq_44715376/article/details/135099143
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。