场景:在很多时候项目在业务中需要查询多个数据库。如何在Springboot支持多数据源的支持呢?
- 支持多个数据源注入
- 支持分库操作(根据业务的逻辑决定SQL到那个数据库执行,具体在代码里是从哪个DataSource中获取Connection)
- 分布式事务支持
多数据源的支持
- 核心:改造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;
}
}
- 上面提到可以通过实现
AbstractRoutingDataSource
的determineCurrentLookupKey
暴露当前的数据源的索引/名称。那么这个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;
}
通过上面两端代码可以了解到多数据源支持的核心
- Spring在获取JDBC连接的时候获取当前数据源的key
- 根据key获取DataSource对象,得到连接
- 执行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
- mybatis - mybatis支持集成
org.apache.ibatis.plugin.Interceptor
实现自定义的插件,这个插件,可以在query
和udpate
方法上做切面。
@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})})
- jooq - jooq实现
IJooqConfigMgr
,通过key获取JooqConfig
对象
JooqConfig jooqConfig = jooqConfigMgr.getDslConfig("default");
- jpa - jpa没有提供自定义切面的方法,只能在
CrudRepository
的方法上加切面。
@Pointcut("execution(public * org.springframework.data.repository.CrudRepository+.*(..))")
private void readWritePoint() { }
分布式事务支持
由于业务比较简单(数据更新操作不跨库执行),并不用去集成jta。但需要保证单个库的更新操作必须是事务支持的。