共计 7923 个字符,预计需要花费 20 分钟才能阅读完成。
大家好,我是课代表。
欢送关注我的公众号:Java 课代表。
上篇介绍了数据源根底,并实现了基于两套 DataSource
,两套mybatis
配置的多数据源,从基础知识层面论述了多数据源的实现思路。不理解的同学请戳→同学,你的多数据源事务生效了!
正如文末回顾所讲,这种形式的多数据源对代码侵入性很强,每个组件都要写两套,不适宜大规模线上实际。
对于多数据源需要,Spring
早在 2007 年就留神到并且给出了解决方案,原文见:dynamic-datasource-routing
Spring
提供了一个 AbstractRoutingDataSource
类,用来实现对多个 DataSource
的按需路由,本文介绍的就是基于此形式实现的多数据源实际。
一、什么是AbstractRoutingDataSource
先看类上的正文:
Abstract {@link javax.sql.DataSource} implementation that routes {@link #getConnection()}
calls to one of various target DataSources based on a lookup key. The latter is usually
(but not necessarily) determined through some thread-bound transaction context.
课代表翻译:这是一个抽象类,能够通过一个 lookup key
,把对getConnection()
办法的调用,路由到指标DataSource
。后者(指lookup key
)通常是由和线程绑定的上下文决定的。
这段正文堪称字字珠玑,没有一句废话。下文联合次要代码解释其含意。
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
// 指标 DataSource Map,能够装很多个 DataSource
@Nullable
private Map<Object, Object> targetDataSources;
@Nullable
private Map<Object, DataSource> resolvedDataSources;
//Bean 初始化时,将 targetDataSources 遍历并解析后放入 resolvedDataSources
@Override
public void afterPropertiesSet() {if (this.targetDataSources == null) {throw new IllegalArgumentException("Property'targetDataSources'is required");
}
this.resolvedDataSources = CollectionUtils.newHashMap(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()
*/
// 依据 #determineCurrentLookupKey()返回的 lookup key 去解析好的数据源 Map 里取相应的数据源
protected DataSource determineTargetDataSource() {Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
// 以后 lookupKey 的值由用户本人实现↓
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.
*/
// 该办法用来决定 lookup key,通常用线程绑定的上下文来实现
@Nullable
protected abstract Object determineCurrentLookupKey();
// 省略其余代码...
}
首先看类图
是个 DataSource
,并且实现了InitializingBean
,阐明有Bean
的初始化操作。
其次看实例变量
private Map<Object, Object> targetDataSources;
和 private Map<Object, DataSource> resolvedDataSources;
其实是一回事,后者是通过对前者的解析得来的,实质就是用来存储多个 DataSource
实例的 Map
。
最初看外围办法
应用 DataSource
,实质就是调用其getConnection()
办法取得连贯,从而进行数据库操作。
AbstractRoutingDataSource#getConnection()
办法首先调用 determineTargetDataSource()
,决定应用哪个指标数据源,并应用该数据源的getConnection()
连贯数据库:
@Override
public Connection getConnection() throws SQLException {return determineTargetDataSource().getConnection();}
protected DataSource determineTargetDataSource() {Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
// 这里应用的 lookupKey 就能决定返回的数据源是哪个
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;
}
所以重点就是 determineCurrentLookupKey()
办法,该办法是形象办法,由用户本人实现,通过扭转其返回值,管制返回不同的数据源。用表格示意如下:
lookupKey | DataSource |
---|---|
first | firstDataSource |
second | secondDataSource |
如何实现这个办法呢?联合 Spring
在正文里给的提醒:
后者(指
lookup key
)通常是由和线程绑定的上下文决定的。
应该能联想到 ThreadLocal
了吧!ThreadLocal
能够保护一个与以后线程绑定的变量,充当这个线程的上下文。
二、实现
设计 yaml
文件内部化配置多个数据源
spring:
datasource:
first:
driver-class-name: org.h2.Driver
jdbc-url: jdbc:h2:mem:db1
username: sa
password:
second:
driver-class-name: org.h2.Driver
jdbc-url: jdbc:h2:mem:db2
username: sa
password:
创立 lookupKey
的上下文持有类:
/**
* 数据源 key 上下文
* 通过管制 ThreadLocal 变量 LOOKUP_KEY_HOLDER 的值用于控制数据源切换
* @see RoutingDataSource
* @author :Java 课代表
*/
public class RoutingDataSourceContext {private static final ThreadLocal<String> LOOKUP_KEY_HOLDER = new ThreadLocal<>();
public static void setRoutingKey(String routingKey) {LOOKUP_KEY_HOLDER.set(routingKey);
}
public static String getRoutingKey() {String key = LOOKUP_KEY_HOLDER.get();
// 默认返回 key 为 first 的数据源
return key == null ? "first" : key;
}
public static void reset() {LOOKUP_KEY_HOLDER.remove();
}
}
实现AbstractRoutingDataSource
:
/**
* 反对动静切换的数据源
* 通过重写 determineCurrentLookupKey 实现数据源切换
* @author :Java 课代表
*/
public class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {return RoutingDataSourceContext.getRoutingKey();
}
}
给咱们的 RoutingDataSource
初始化上多个数据源:
/**
* 数据源配置
* 把多个数据源,拆卸到一个 RoutingDataSource 里
* @author :Java 课代表
*/
@Configuration
public class RoutingDataSourcesConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.first")
public DataSource firstDataSource() {return DataSourceBuilder.create().build();}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.second")
public DataSource secondDataSource() {return DataSourceBuilder.create().build();}
@Primary
@Bean
public RoutingDataSource routingDataSource() {RoutingDataSource routingDataSource = new RoutingDataSource();
routingDataSource.setDefaultTargetDataSource(firstDataSource());
Map<Object, Object> dataSourceMap = new HashMap<>();
dataSourceMap.put("first", firstDataSource());
dataSourceMap.put("second", secondDataSource());
routingDataSource.setTargetDataSources(dataSourceMap);
return routingDataSource;
}
}
演示一下手工切换的代码:
public void init() {
// 手工切换为数据源 first,初始化表
RoutingDataSourceContext.setRoutingKey("first");
createTableUser();
RoutingDataSourceContext.reset();
// 手工切换为数据源 second,初始化表
RoutingDataSourceContext.setRoutingKey("second");
createTableUser();
RoutingDataSourceContext.reset();}
这样就实现了最根本的多数据源切换了。
不难发现,切换工作很显著能够抽成一个切面,咱们能够优化一下,利用注解表明切点,哪里须要切哪里。
三、引入AOP
自定义注解
/**
* @author :Java 课代表
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WithDataSource {String value() default "";
}
创立切面
@Aspect
@Component
// 指定优先级高于 @Transactional 的默认优先级
// 从而保障先切换数据源再进行事务操作
@Order(Ordered.LOWEST_PRECEDENCE - 1)
public class DataSourceAspect {@Around("@annotation(withDataSource)")
public Object switchDataSource(ProceedingJoinPoint pjp, WithDataSource withDataSource) throws Throwable {
// 1. 获取 @WithDataSource 注解中指定的数据源
String routingKey = withDataSource.value();
// 2. 设置数据源上下文
RoutingDataSourceContext.setRoutingKey(routingKey);
// 3. 应用设定好的数据源解决业务
try {return pjp.proceed();
} finally {
// 4. 清空数据源上下文
RoutingDataSourceContext.reset();}
}
}
有了注解和切面,应用起来就不便多了:
// 注解表明应用 "second" 数据源
@WithDataSource("second")
public List<User> getAllUsersFromSecond() {List<User> users = userService.selectAll();
return users;
}
对于切面有两个细节须要留神:
-
须要指定优先级高于申明式事务
起因:申明式事务事务的实质也是 AOP,其只对开启时应用的数据源失效,所以肯定要在切换到指定数据源之后再开启,申明式事务默认的优先级是最低级,这里只须要设定自定义的数据源切面的优先级比它高即可。
-
业务执行完之后肯定要清空上下文
起因:假如办法 A 应用
@WithDataSource("second")
指定走 ”second” 数据源,紧跟着办法 B 不写注解,冀望走默认的first
数据源。但因为办法 A 放入上下文的lookupKey
此时还是 ”second” 并未删除,所以导致办法 B 执行的数据源与冀望不符。
四、回顾
至此,基于 AbstractRoutingDataSource
+AOP
的多数据源就实现好了。
在配置 DataSource
这个Bean
的时候,用的是自定义的 RoutingDataSource
,并且标记为 @Primary
。这样就能够让mybatis-spring-boot-starter
应用 RoutingDataSource
帮咱们主动配置好 mybatis
,比搞两套DataSource
+ 两套Mybatis
配置的计划简略多了。
文中相干代码已上传课代表的 github
特地阐明:
样例中为了缩小代码层级,让展现更直观,在 controller 层写了事务注解,理论开发中可别这么干,controller 层的工作是绑定、校验参数,封装返回后果,尽量不要在外面写业务!
五、优化
对于个别的多数据源应用场景,本文计划已足够笼罩,能够实现灵便切换。
但还是存在如下有余:
- 每个利用应用时都要新增相干类,大量反复代码
- 批改或新增性能时,所有相干利用都得改
- 性能不够强悍,没有高级性能,比方读写拆散场景下的读多个从库负载平衡
其实把这些代码封装到一个 starter
外面,高级性能缓缓扩大就能够。
好在开源世界早就有现成工具可用了,开发 mybatis-plus
的 ”baomidou” 团队在其生态中开源了一个多数据源框架 Dynamic-Datasource,底层原理就是AbstractRoutingDataSource
,减少了更多强悍的扩大性能,下篇介绍其应用。