对多数据源大家应该不陌生,一般的在单个应用都会存在一个数据库,一个文件存储。这里所说的数据库就是我们描述的数据源。那么多数据源的意思其实通俗来讲就是在一个单体应用中存在两个以上的数据库。这个时候就需要我们对多个数据源进行分别对待进行处理了。
理解多数据源的概念
在之前的文章中我们了解了在Spring Boot如何整合单个数据源,并且我们也提到了数据源整合时候的原理。下面我们就依托于之前提到的原理来构建我们的多数据源应用。
之前文章结尾的时候我们提到了在整合Druid数据库连接池的时候,在MyBatis自动注入的注解上有如下一个配置。
@ConditionalOnSingleCandidate(DataSource.class)
而对于@ConditionalOnSingleCandidate注解,我们在讲条件注解的时候曾经提及到,它的意思就是在容器中只有一个DataSource实例的时候,这个自动配置类才会被加载生效。这就与我们上面提到的概念相互冲突了。为什么这么说呢?
因为根据这个注解的意思,容器中只能存在一个数据源,如果需要多个数据源那就不能使用MyBatis框架来进行整合了。那岂不是什么事情都干不了呢?根据字面的意思是可以这样理解的,但是实际上并不是这样。
多数据源的意思其实并不是在同一时刻有多个数据源同时被操作。在Spring框架中,提供了一个AbstractRoutingDataSource的类,根据字面意思,是用来进行数据源路由的,路由的意思就是相互之间可以切换。也就是说可以实现数据源之间的动态切换操作。所以被称为是动态数据源操作。所以这里MyBatis框架所支持其实是一个动态数据源的切换而不是想我们所理解的在同一时刻多个数据源共同操作的场景。
既然理解到这里了?那么我们就来研究一下在Spring Boot中如何去实现动态数据源。
动态数据源
根据上面的意思,动态数据源首先能做的事情就是可以实现自由路由操作。也就是说可以随时切换到其中的一个数据源上。在Spring框架中提供了AbstractRoutingDateSource这样一个类。其源码如下
public abstract class AbstractRoutingDataSource
extends AbstractDataSource implements InitializingBean {
@Nullable
private Map<Object, Object> targetDataSources;
会看到在这个类中有如下一个属性。
private Map<Object, Object> targetDataSources;
这个属性是一个Map结构,也就是说可以用KV键值对来表示。其中K表示需要切换的规则,而V则表示所要切换的数据源。
自习研究这个类我们会发现在这个抽象类中大部分的方法都被实现了,但是唯独在结束的时候有如下这样一个方法没有被实现。需要等待其继承类来实现
/**
* 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();
根据上面的英文描述结合自己的理解,我们可以知道,这个的返回值就决定了我们需要切换的数据源所选择的Key是什么。也就是说我们通过这里获取到Key从targetDataSources中获取到对应的数据源的值。
到这里我们就很容易理解数据源的切换逻辑,并且很好的理解动态数据源在SpringBoot中是如何实现的了?那么既然数据源是属于一个共享资源,那么我们就必须要去考虑到多线程线程安全的问题?在Spring Boot中是如何保证数据源线程安全的呢?
由数据源引出的线程安全问题
既然数据源作为一个多线程共享资源,那么在多线程的情况下如何实现线程安全的呢?也就是说不会出现A线程刚刚正在A数据源修改数据,这个时候B线程也去修改数据了,它所对应的是B数据源,两个线程出现线程安全问题,就会出现要不两个数据库中都没有数据,要不就是A库中的数据存入到了B库中的情况。那么这个时候就需要线程隔离来参与。
我们常见的线程隔离手段就是通过ThreadLocal类来是实现,也就是说,当我们获取到切换到的数据源KEY的时候,从targetDataSources获取到的值是存储在ThreadLocal中的,ThreadLocal中的值是对于每个线程来讲是独立的,用完之后直接清理就可以了。
那么既然这样我们可以实现如下的一个处理器用来完成Key的选择操作。
public class DynamicDataSourceContextHolder
{
public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);
/**
* 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
* 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
*/
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
/**
* 设置数据源的变量
*/
public static void setDataSourceType(String dsType)
{
log.info("切换到{}数据源", dsType);
CONTEXT_HOLDER.set(dsType);
}
/**
* 获得数据源的变量
*/
public static String getDataSourceType()
{
return CONTEXT_HOLDER.get();
}
/**
* 清空数据源变量
*/
public static void clearDataSourceType()
{
CONTEXT_HOLDER.remove();
}
}
接下来就是如何实现一个动态数据源的构造。我们可以继承AbstractRoutingDataSource类来构造一个动态数据源,根据上面提到的内容只需要对determineCurrentLookupKey()方法进行重写即可代码如下
public class DynamicDataSource extends AbstractRoutingDataSource
{
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources)
{
super.setDefaultTargetDataSource(defaultTargetDataSource);
super.setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
}
@Override
protected Object determineCurrentLookupKey()
{
return DynamicDataSourceContextHolder.getDataSourceType();
}
}
到这里整个的动态数据源相关的内容就结束了,相对来说比较抽象。但是可以帮助我们理解在有些配置中设置一个主从数据源的原理。
接下来就是如何使用动态数据源的操作了
如何使用动态数据源
首先需要在配置类中注入我们设置好的数据源,代码如下
@Configuration
public class DruidConfig
{
@Bean
@ConfigurationProperties("spring.datasource.druid.master")
public DataSource masterDataSource(DruidProperties druidProperties)
{
DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
return druidProperties.dataSource(dataSource);
}
@Bean
@ConfigurationProperties("spring.datasource.druid.slave")
@ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true")
public DataSource slaveDataSource(DruidProperties druidProperties)
{
DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
return druidProperties.dataSource(dataSource);
}
@Bean(name = "dynamicDataSource")
@Primary
public DynamicDataSource dataSource(DataSource masterDataSource)
{
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);
setDataSource(targetDataSources, DataSourceType.SLAVE.name(), "slaveDataSource");
return new DynamicDataSource(masterDataSource, targetDataSources);
}
/**
* 设置数据源
*
* @param targetDataSources 备选数据源集合
* @param sourceName 数据源名称
* @param beanName bean名称
*/
public void setDataSource(Map<Object, Object> targetDataSources, String sourceName, String beanName)
{
try
{
DataSource dataSource = SpringUtils.getBean(beanName);
targetDataSources.put(sourceName, dataSource);
}
catch (Exception e)
{
}
}
}
简单的理解一下,就是首先注入了两个数据源,一个是Master数据源,一个Slave数据源,并且将其交给了我们自定义的动态数据源操作进行管理
总结
到这里,动态数据源相关的内容就结束了,我们通过一个小例子来实现了动态数据源的切换,这里我们配置了一个主从数据源,当然我们也可以根据实际情况配置更多的数据源,这个时候就需要我们数据源之间做一个主从同步的功能,保证我们所有数据源中的数据的一致性,这样才能更好的服务于我们的应用。