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讲解一下如何实现动态切换数据源。

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