AbstractRoutingDataSource 動態切換數據源

在項目中,有時候需要配置多數據源,在不同的場景下需要查詢不同的數據庫來滿足業務需求。在傳統的Spring + Mybatis 項目中,配置一個數據源如下:

	<!-- 配置數據源 -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.driver}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>

    <!-- 註冊sqlSessionFactory -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="configLocation" value="classpath:config/mybatis-config.xml"/>
        <property name="typeAliasesPackage" value="com.wch.base.domain"/>
        <property name="mapperLocations" value="classpath:mapper/*.xml"/>
    </bean>

    <!-- 配置dao接口掃描,配置sqlSessionTemplate -->
    <context:component-scan base-package="com.wch.base.mapper"/>
    <bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
        <constructor-arg ref="sqlSessionFactory"/>
    </bean>

dataSource -> sqlSessionFactory-> sqlSessionTemplate ,三者是相呼應的,所以要配置多數據源,需要把DataSource、SqlSessionFactory、SqlSessionTemplate都配置多次,當數據源數量升上去以後,很難維護和管理,還容易出現問題。

AbstractRoutingDataSource

AbstractRoutingDataSource是spring-jdbc包提供的一個了AbstractDataSource的抽象類,它實現了DataSource接口的用於獲取數據庫連接的方法。


	@Override
	public Connection getConnection(String username, String password) throws SQLException {
		return determineTargetDataSource().getConnection(username, password);
	}

AbstractRoutingDataSource 的源碼介紹:
在這裏插入圖片描述AbstractRoutingDataSource 的 getConnection() 方法會根據 lookup 對應不同目標的數據源,通常是(但不一定)通過某些線程綁定的事物上下文來實現。

實現原理:

  1. AbstractRoutingDataSource 中的 targetDataSources 用來存放需要配置個多數據源(key 可以自己定義),defaultTargetDataSource 用來存放默認的數據源。
  2. 在 afterPropertiesSet() 方法中會把 targetDataSources 中的數據源存放到 resolvedDataSources 中,把默認的數據源 defaultTargetDataSource 存放到 resolvedDefaultDataSource 中。
  3. 調用 determineCurrentLookupKey() 接口獲取當前需要使用的數據源 key,拿到對應的 DataSource 然後調用 getConnection() 返回數據庫連接。
	@Override
	public void afterPropertiesSet() {
		if (this.targetDataSources == null) {
			throw new IllegalArgumentException("Property 'targetDataSources' is required");
		}
		this.resolvedDataSources = new HashMap<>(this.targetDataSources.size());
		this.targetDataSources.forEach((key, value) -> {
			Object lookupKey = resolveSpecifiedLookupKey(key);
			DataSource dataSource = resolveSpecifiedDataSource(value);
			this.resolvedDataSources.put(lookupKey, dataSource);
		});
		if (this.defaultTargetDataSource != null) {
			this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
		}
	}
	
	@Override
	public Connection getConnection() throws SQLException {
		return determineTargetDataSource().getConnection();
	}

	/**
	 * 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");
		// 獲取 key 
		Object lookupKey = determineCurrentLookupKey();
		// 獲取當前 key 對應的數據源
		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();

整個流程很清晰,現在寫一個實現類,重寫 determineCurrentLookupKey() 接口。


/**
 * 數據源動態切換路由
 */
public class RoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSourceName();
    }
}

DataSourceContextHolder:


/**
 * 數據源上下文
 */
@Slf4j
@UtilityClass
public class DataSourceContextHolder {
    /**
     * 線程級別的私有變量
     */
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

    public String getDataSourceName() {
        return CONTEXT_HOLDER.get();
    }

    public void setDataSourceName(String name) {
        log.info("切換到:{}數據源", name);
        CONTEXT_HOLDER.set(name);
    }

    public void clearDataSource() {
        CONTEXT_HOLDER.remove();
    }
}

在查詢數據庫前,調用 DataSourceContextHolder 的 setDataSourceName() 接口,設定要查詢的數據源的 key,在 determineCurrentLookupKey() 中將存放的 key 返回即可。

本篇文章沒有實例分析,只是簡單介紹了 AbstractRoutingDataSource 的實現原理,下一篇文章配合AOP講解一下如何實現動態切換數據源。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章