Spring多數據源動態切換


原理


DataSource向外提供一個 getConnection() 方法,得getConnection者得數據庫

AbstractRoutingDataSource 實現了 getConnection() 方法

	// line 166
	@Override
	public Connection getConnection() throws SQLException {
		return determineTargetDataSource().getConnection();
	}

	... 省略若干代碼 
        
    // line 190
    /**
	 * Retrieve the current target DataSource. Determines the
	 * {@link #determineCurrentLookupKey() current lookup key}, performs
	 * a lookup in the {@link #setTargetDataSources targetDataSources} map,
	 * falls back to the specified
	 * {@link #setDefaultTargetDataSource default target DataSource} if necessary.
	 * @see #determineCurrentLookupKey()
	 */
	protected DataSource determineTargetDataSource() {
		Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
		Object lookupKey = determineCurrentLookupKey();
		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 + "]");
		}
		return dataSource;
	}

	/**
	 * Determine the current lookup key. This will typically be
	 * implemented to check a thread-bound transaction context.
	 * <p>Allows for arbitrary keys. The returned key needs
	 * to match the stored lookup key type, as resolved by the
	 * {@link #resolveSpecifiedLookupKey} method.
	 */
	@Nullable
	protected abstract Object determineCurrentLookupKey();

然而 ....

AbstractRoutingDataSource 的getConnection() 方法只是調用了 determinTargetDataSource().getConnection() 來獲取真正DataSource的getConnection()。

這是典型的裝飾模式!!自己沒有的功能通過引入其他類來增強。

我們先來看看 AbstractRoutingDataSource 的類結構

被框框套住的都是重要的。

方法determineCurrentLookupKey() 是留給我們開發者的(就像你家的網線口),我們通過實現該方法在不同數據源之間切換。

實踐

1. 配置多數據源

在 application.yml 如下配置

spring:
  datasource:
    # 數據源類型
    type: com.alibaba.druid.pool.DruidDataSource
    # 默認數據源
    default-datasource:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/db0?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&allowMultiQueries=true&serverTimezone=GMT%2B8
      username: root
      password: 123456

    # 多數據源
    target-datasources:
      datasource1:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/db1?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&allowMultiQueries=true&serverTimezone=GMT%2B8
        username: root
        password: 123456

      datasource2:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/db2?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&allowMultiQueries=true&serverTimezone=GMT%2B8
        username: root
        password: 123456

    # druid 默認配置
    druid:
      # 初始連接數
      initial-size: 10
      # 最大連接池數量
      max-active: 100
      # 最小連接池數量
      min-idle: 10
      # 配置獲取連接等待超時的時間
      max-wait: 60000
      # 打開PSCache,並且指定每個連接上PSCache的大小
      pool-prepared-statements: true
      max-pool-prepared-statement-per-connection-size: 20
      # 配置間隔多久才進行一次檢測,檢測需要關閉的空閑連接,單位是毫秒
      timeBetweenEvictionRunsMillis: 60000
      # 配置一個連接在池中最小生存的時間,單位是毫秒
      min-evictable-idle-time-millis: 300000
      validation-query: SELECT 1 FROM DUAL
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false
      stat-view-servlet:
        enabled: true
        url-pattern: /monitor/druid/*
      filter:
        stat:
          log-slow-sql: true
          slow-sql-millis: 1000
          merge-sql: false
        wall:
          config:
            multi-statement-allow: true

# MyBatis
mybatis:
  # 搜索指定包別名
  typeAliasesPackage: com.liuchuanv
  # 配置mapper的掃描,找到所有的mapper.xml映射文件
  mapperLocations: classpath*:mapper/**/*Mapper.xml
  # 加載全局的配置文件
  configLocation: classpath:mybatis-config.xml

此處配置的名稱(如 defaultDataSource、targetDataSources)的命名並無特殊要求,只要和下面第n步的 DataSourceConfig 中對應起來就可以

使用 Druid 數據源的話,要在 pom.xml 中引入依賴

    <!--阿里數據庫連接池 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.1.10</version>
    </dependency>

2. 實現動態數據源

DynamicDataSource 動態數據源,在多個數據源之間切換

public class DynamicDataSource extends AbstractRoutingDataSource {

    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }

    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSourceType();
    }
}

DataSourceContextHolder 數據源上下文,使用線程變量來存儲代表當前使用的數據源的key值(每個key值都對應一個數據源,用以區分多數據源)

public class DataSourceContextHolder {

    public static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<String>();

    public static void setDataSourceType(String dsType) {
        CONTEXT_HOLDER.set(dsType);
    }

    public static String getDataSourceType() {
        return CONTEXT_HOLDER.get();
    }

    public static void removeDataSourceType() {
        CONTEXT_HOLDER.remove();
    }

}

DataSourceType 數據源對應的key(其實單純的用字符串來表示數據源,替換枚舉類DataSourceType也是可以的,但是寫代碼時要注意字符串統一)

public enum  DataSourceType {
    /** 默認數據源key */
    DEFAULT_DATASOURCE,

    /** 數據源1key*/
    DATASOURCE1,

    /** 數據源2key*/
    DATASOURCE2;
}

3. 將數據源添加到 Spring 容器中

@Configuration
public class DataSourceConfig {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.default-datasource")
    public DataSource defaultDataSource() {
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.target-datasources.datasource1")
    public DataSource dataSource1() {
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.target-datasources.datasource2")
    public DataSource dataSource2() {
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    @Primary
    public DataSource dynamicDataSource(DataSource defaultDataSource, DataSource dataSource1, DataSource dataSource2) {
        // 注意:該方法的參數名稱要和前面前面三個datasource對象在Spring容器中的bean名稱一樣
        // 或者使用 @Qualifier 指定具體的bean
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceType.DEFAULT_DATASOURCE.name(), defaultDataSource);
        targetDataSources.put(DataSourceType.DATASOURCE1.name(), dataSource1);
        targetDataSources.put(DataSourceType.DATASOURCE2.name(), dataSource2);
        return new DynamicDataSource(defaultDataSource, targetDataSources);
    }
}

測試

為了方便,省略了 Service 層

TestController

@RestController
@RequestMapping("/test")
public class TestController {

    @Autowired
    private TestMapper testMapper;

    @GetMapping
    public List<Map<String, Object>> test(String dataSourceIndex) {
        // 根據參數值的不同,切換數據源
        if ("1".equals(dataSourceIndex)) {
            DataSourceContextHolder.setDataSourceType(DataSourceType.DATASOURCE1.name());
        } else if ("2".equals(dataSourceIndex)) {
            DataSourceContextHolder.setDataSourceType(DataSourceType.DATASOURCE2.name());
        }
        List<Map<String, Object>> mapList = testMapper.selectList();
        // 清除線程內部變量數據源key
        DataSourceContextHolder.removeDataSourceType();
        return mapList;
    }
}

TestMapper

@Repository
public interface TestMapper {
    /**
     * 查詢列表
     * @return
     */
    List<Map<String, Object>> selectList();
}

TestMapper.xml

<?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.liuchuanv.dynamicdatasource.mapper.TestMapper">
	<select id="selectList" resultType="java.util.Map">
		SELECT * FROM test
	</select>
</mapper>

別忘了要准備數據哦!

下面SQL語句,創建3個數據庫,然后在3個數據庫中都創建一張test表,並各自插入不同的數據。


-- 創建數據庫
create database db0 character set utf8 collate utf8_general_ci;
create database db1 character set utf8 collate utf8_general_ci;
create database db2 character set utf8 collate utf8_general_ci;

-- 在數據庫db1下執行以下SQL
use db0;
create table test(
	id int(11) primary key auto_increment,
	name varchar(20)
) ;
insert into test(name) values('張三');


-- 在數據庫db1下執行以下SQL
use db1;
create table test(
	id int(11) primary key auto_increment,
	name varchar(20)
) ;
insert into test(name) values('李四');

-- 在數據庫db2下執行以下SQL
use db2;
create table test(
	id int(11) primary key auto_increment,
	name varchar(20)
) ;
insert into test(name) values('王五');

OK,一切准備就緒,啟動應用吧!!!

一啟動就出現了各種各樣的,似乎無窮無盡的報錯!一頭黑線。

1. 找不到TestMapper

Field testMapper in com.liuchuanv.dynamicdatasource.controller.TestController required a bean of type 'com.liuchuanv.dynamicdatasource.mapper.TestMapper' that could not be found.

解決方法:在 DynamicdatasourceApplication 頭上添加注解 @MapperScan("com.liuchuanv.*.mapper")

2. dynamicDataSource 依賴循環

┌─────┐
|  dynamicDataSource defined in class path resource [com/liuchuanv/dynamicdatasource/common/DataSourceConfig.class]
↑     ↓
|  defaultDataSource defined in class path resource [com/liuchuanv/dynamicdatasource/common/DataSourceConfig.class]
↑     ↓
|  org.springframework.boot.autoconfigure.jdbc.DataSourceInitializerInvoker
└─────┘

解決方法:在 DynamicdatasourceApplication 頭上修改注解 @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})

終於處理好所有的問題,終於能痛痛快快的訪問 http://localhost:8080/test

使用的是默認數據源 defaultDataSource

使用的是數據源 dataSource1

使用的是數據源 dataSource2

建議大家在心里總結一下整個的過程,其實很簡單


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM