一.背景
最近我在做一个代码开发模板,需要实现对数据源的读写分离。也许我们直接在网上搜索了一下,就会发现很多读写分析的方案,其中苞米谷的方案应用最多,最成熟,它也是之前我们开发中常用的,之前老模板的搭建,我仅仅将其通用方法引入了工程模板,并未考虑其原理和实现,仅仅停留在如何使用的层面。今天我们就基于苞米谷的源码,谈谈如何设计一个多数据源访问工具。
二.方案分析
既然我们要实现多数据源访问工具,我们在启动的时候就需要将不同的数据源加载到内存中,后续按照业务逻辑编写需要,在代码中灵活调整数据源访问。从这个思路,可以分为两大步骤,第一步加载配置创建数据源。第二大步实现数据源动态切换。接下来,我们结合苞米谷的源码,一步一步分析其实现逻辑。
三.实现步骤
第一步加载配置创建数据源
3.1:找到入口,启动加载
启动的时候,Springboot启动器,会加载spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration
说明程序是从DynamicDataSourceAutoConfiguration开始的,我们就从它开始
3.2:掌握DynamicDataSourceAutoConfiguration做了什么事情
首先看类上注解
@Configuration
@EnableConfigurationProperties({DynamicDataSourceProperties.class})
@AutoConfigureBefore({DataSourceAutoConfiguration.class})
@Import({DruidDynamicDataSourceConfiguration.class, DynamicDataSourceCreatorAutoConfiguration.class})
@ConditionalOnProperty(prefix = "spring.datasource.dynamic",name = {"enabled"},havingValue = "true",matchIfMissing = true
)
注解的含义:
- DynamicDataSourceProperties 就是我们在yml或者properties可以配置的属性值,这些配置会被映射到该类对象中。
- @Configuration 说明它是一个配置类,会在程序运行的时候自动加载
- AutoConfigureBefore,为了防止和SpringBoot默认的启动器
DataSourceAutoConfiguration
冲突,设置该配置要在其自动配置之前进行配置。 - Import,像容器中注入2个配置的BeanDefinition:
- DruidDynamicDataSourceConfiguration:复用Druid的自动配置
- DynamicDataSourceCreatorAutoConfiguration:该配置类,主要为了往容器注入DataSource创建器的bean。有4种创建器(默认,JNDI,Druid,Hikari)
- ConditionalOnProperty 表明了可以通过配置
spring.datasource.dynamic.enable=false
来关闭动态数据源配置
其次,该类使用@ConditionalOnMissingBean的配置,向类中注入了一些bean,其中就包括从配置读取数据源配置的bean,实现对配置内容的读取,接下来我们看一下配置加载类,因为只有加载了配置,才能去创建对应的数据源
3.3:加载多数据源配置
public class YmlDynamicDataSourceProvider extends AbstractDataSourceProvider implements DynamicDataSourceProvider {private static final Logger log = LoggerFactory.getLogger(YmlDynamicDataSourceProvider.class);private Map<String, DataSourceProperty> dataSourcePropertiesMap;public Map<String, DataSource> loadDataSources() {return this.createDataSourceMap(this.dataSourcePropertiesMap);}@ConstructorProperties({"dataSourcePropertiesMap"})public YmlDynamicDataSourceProvider(Map<String, DataSourceProperty> dataSourcePropertiesMap) {this.dataSourcePropertiesMap = dataSourcePropertiesMap;}
}
该类主要是将配置加载到程序中,存放到一个Map<String, DataSourceProperty> 里面,其中的key为不同的数据源,DataSourceProperty是数据源对应的配置信息
3.4:利用上一步加载到Map<String, DataSourceProperty>的信息,创建数据源
protected Map<String, DataSource> createDataSourceMap(Map<String, DataSourceProperty> dataSourcePropertiesMap) {Map<String, DataSource> dataSourceMap = new HashMap(dataSourcePropertiesMap.size() * 2);Iterator var3 = dataSourcePropertiesMap.entrySet().iterator();while(var3.hasNext()) {Map.Entry<String, DataSourceProperty> item = (Map.Entry)var3.next();DataSourceProperty dataSourceProperty = (DataSourceProperty)item.getValue();String pollName = dataSourceProperty.getPoolName();if (pollName == null || "".equals(pollName)) {pollName = (String)item.getKey();}dataSourceProperty.setPoolName(pollName);dataSourceMap.put(pollName, this.dataSourceCreator.createDataSource(dataSourceProperty));}return dataSourceMap;}
从代码中可以看到使用迭代器方法,不断从Map中读取每一个数据源的配置信息,然后不断创建DataSource,然后将创建出来的DataSource存入一个Map<String,DataSource>的集合里面,其中的key为不同的数据源,DataSource是不同的数据源。
小结一下,整体思路我们捋一下
从DynamicDataSourceAutoConfiguration开始进行自动配置,加载数据源配置信息,然后将配置信息转换为对应的DataSource,存在一个map里面。
第二大步骤,如何实现数据源切换的,它的原理是什么?
我们使用过苞米谷的数据源切换,都知道,主要是通过@DS注解的方式来实现数据源切换,其实它的逻辑主要走AOP的切面逻辑
4.1 DynamicDataSourceAnnotationAdvisor的逻辑
private Pointcut buildPointcut() {Pointcut cpc = new AnnotationMatchingPointcut(DS.class, true);Pointcut mpc = AnnotationMatchingPointcut.forMethodAnnotation(DS.class);return (new ComposablePointcut(cpc)).union(mpc);}
这里面主要是对@DS进行拦截,具体的处理是在DynamicDataSourceAnnotationInterceptor
4.2 DynamicDataSourceAnnotationInterceptor的逻辑
public Object invoke(MethodInvocation invocation) throws Throwable {Object var2;try {DynamicDataSourceContextHolder.push(this.determineDatasource(invocation));var2 = invocation.proceed();} finally {DynamicDataSourceContextHolder.poll();}return var2;}private String determineDatasource(MethodInvocation invocation) throws Throwable {Method method = invocation.getMethod();DS ds = method.isAnnotationPresent(DS.class) ? (DS)method.getAnnotation(DS.class) : (DS)AnnotationUtils.findAnnotation(RESOLVER.targetClass(invocation), DS.class);String key = ds.value();return !key.isEmpty() && key.startsWith("#") ? this.dsProcessor.determineDatasource(invocation, key) : key;}
这两个方法就是核心,一个是实现切换数据源,一个是决定数据源,由代码可知,决定切换到那个数据源是由key句定的,那么这个可以就是在配置文件中配置的数据源名称。
重点逻辑1.数据源是如何切换的呢?
切换数据源 DynamicDataSourceContextHolder类中
private static final ThreadLocal<Deque<String>> LOOKUP_KEY_HOLDER = new NamedInheritableThreadLocal("dynamic-datasource") {protected Object initialValue() {return new ArrayDeque();}};
该ThreadLocal相当于为每个线程分配了一个ArrayDeque队列,虽然是队列,但是它是拿来当栈使用的。至于原因,因为ArrayDeque的效率比Stack要高。
为什么必须是栈
因为我们的调用往往是嵌套的:A->B->C 当C执行完了,数据源就应该切回B的数据源了,所以应该用栈结构实现。
小结一下
实现数据源的切换是通过AOP实现的,里面用到了数据结构ArrayDeque队列实现了栈的结构,确保数据源嵌套调用的完整性,不至于混乱。