Springboot 多数据源支持

1,367 阅读2分钟

场景:在很多时候项目在业务中需要查询多个数据库。如何在Springboot支持多数据源的支持呢?

  1. 支持多个数据源注入
  2. 支持分库操作(根据业务的逻辑决定SQL到那个数据库执行,具体在代码里是从哪个DataSource中获取Connection)
  3. 分布式事务支持

多数据源的支持

  1. 核心:改造DataSource。自定义继承了AbstractRoutingDataSource的GroupDataSource类,相关介绍可阅读 AbstractRoutingDataSource 。 GroupDataSource实现了AbstractRoutingDataSource的方法,用来获取当前的LookupKey当前的数据源的key。
    @Override
    protected Object determineCurrentLookupKey() {
        String name = DynamicDataSourceContext.getCurrentGroupName();
        if (null != name && name.length() > 0) {
            return name;
        } else {
            return DEFAULT_GROUP;
        }
    }
  1. 上面提到可以通过实现AbstractRoutingDataSourcedetermineCurrentLookupKey暴露当前的数据源的索引/名称。那么这个determineCurrentLookupKey会在什么时候被执行呢?跟多数据源有什么关系呢?

看一下Spring获取JDBC Connection的源码

public static Connection doGetConnection(DataSource dataSource) throws SQLException {
	Assert.notNull(dataSource, "No DataSource specified");

	ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
	if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
		conHolder.requested();
		if (!conHolder.hasConnection()) {
			logger.debug("Fetching resumed JDBC Connection from DataSource");
			conHolder.setConnection(dataSource.getConnection());
		}
		return conHolder.getConnection();
	}
}

进到TransactionSynchronizationManager.getResource(dataSource),这里的dataSource是注入到SpringContext的自定义dataSource。

org.springframework.transaction.support.TransactionSynchronizationManager
/**
 * Retrieve a resource for the given key that is bound to the current thread.
 * @param key the key to check (usually the resource factory)
 * @return a value bound to the current thread (usually the active
 * resource object), or {@code null} if none
 * @see ResourceTransactionManager#getResourceFactory()
 */
public static Object getResource(Object key) {
	Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
	Object value = doGetResource(actualKey);
	if (value != null && logger.isTraceEnabled()) {
		logger.trace("Retrieved value [" + value + "] for key [" + actualKey + "] bound to thread [" +
				Thread.currentThread().getName() + "]");
	}
	return value;
}

通过上面两端代码可以了解到多数据源支持的核心

  1. Spring在获取JDBC连接的时候获取当前数据源的key
  2. 根据key获取DataSource对象,得到连接
  3. 执行SQL

分库逻辑

分库的逻辑是对上面提到的key做动作。例如:线程A要从商品库查询商品列表,线程B要从库存表查询对应的库存。我们将当期会话/逻辑需要命中的DataSource的key存储到ThreadLocal中,在Spring获取JDBC连接的时候从TheadLocal中拿到key,并通过key从DateSourceGroup中获取DataSource对象。

private static final ThreadLocal<String> currentLookupKey = new ThreadLocal<>();

public static void clear() {
    currentLookupKey.remove();
    HintManagerHolder.clear();
}

public static String getCurrentLookupKey() {
    return currentGroupName.get();
}

public static void setCurrentLookupKey(String name) {
    currentGroupName.set(name);
}

那么如何去修改或是初始化这个key呢? 关键词:AOP

  1. mybatis - mybatis支持集成org.apache.ibatis.plugin.Interceptor实现自定义的插件,这个插件,可以在queryudpate方法上做切面。
@Intercepts({
        @Signature(type = Executor.class, method = "update", args = {
                MappedStatement.class, Object.class}),
        @Signature(type = Executor.class, method = "query", args = {
                MappedStatement.class, Object.class, RowBounds.class,
                ResultHandler.class})})
  1. jooq - jooq实现IJooqConfigMgr,通过key获取JooqConfig对象
JooqConfig jooqConfig = jooqConfigMgr.getDslConfig("default");
  1. jpa - jpa没有提供自定义切面的方法,只能在CrudRepository的方法上加切面。
@Pointcut("execution(public * org.springframework.data.repository.CrudRepository+.*(..))")
    private void readWritePoint() { }

分布式事务支持

Spring 分布式事务配置 (atomikos)

由于业务比较简单(数据更新操作不跨库执行),并不用去集成jta。但需要保证单个库的更新操作必须是事务支持的。