主要需求
在一个Springboot项目中需要使用到多个数据源(连接到多个不同的数据库)。项目中使用MybatisPlus作为持久层方案。需要做到类似于位某个Mybatis Mapper加不同的注解,就能够让该mapper连接到不同数据源。如下图:
@Mapper
public interface TestMapper {
@Select("XXXXXX")
void Test();
@Insert("XXXXXX")
@Datasource("db-test1")
void Test1();
@Select("XXXXXX")
@Datasource("db-test2")
void test2();
@Select("XXXXXX")
@Datasource("db-test3")
void test3();
}
有一个注解名字叫做@Datasource,该注解可以配置在class上面, 也可以配置在method上面。当配置在class上面, 表示当前mapper里面的所有method都使用class所配置的数据源。而配置在method上面表示当前method使用配置的数据源。如果class和method都配置有,依method的为准。如果class和method都没有@Datasource,表示使用默认数据源连接。
主要实现
我们先理清上述需求,把框架搭建出来。实现一个简单的多数据源查询。这里我们先不考虑注解方式来切换数据源,因为使用注解方式切换数据源,就意味着我们需要一个切面,在每次调用Mapper方法之前都要在切面中检查这个注解的value是什么,然后再将这个value作为数据源的key来获取数据源。我们来做个简单的实现,就是再Service调用Mapper方法之前,每次都手动去切换到当前方法需要的数据源。类似下面的结构。
@Service
public class TestServiceImpl implements TestService {
@Autowired
private TestMapper testMapper;
@Override
public void test(User user) {
testMapper.insert(user);
}
@Override
public void test1(User user) {
XXXXX.setDataSourceName("db-test1");
testMapper.updateById(user);
}
@Override
public void test2(User user) {
XXXXX.setDataSourceName("db-test2");
testMapper.updateById(user);
}
}
当我们需要使用到默认数据源来执行mapper时候,就直接使用,而当想要使用某个其他数据源,我们就需要把该数据源名称设置给一个标识,而动态数据源再接收到这个标识的时候会进行切换。
具体实现
现在我们就开始做以上描述的实现把,在实现的过程中具体再讲解每一步的意思。
1. 创建DynamicDataSourceContext线程安全类
我们先创建下面这个类,这个类是整个实现的基础,我们先来看看这个类中是做什么的。
public class DynamicDataSourceContext {
private static final ThreadLocal<String> HOLDER = new ThreadLocal<>();
/**
* 设置当前数据源的名称
*/
public static void setDataSourceName(String dataSourceName) {
HOLDER.set(dataSourceName);
}
/**
* 获取当前数据源的名称
*/
public static String getDataSourceName() {
return HOLDER.get();
}
/**
* 清除当前数据源的名称
*/
public static void clearDataSourceName() {
HOLDER.remove();
}
}
代码很简单,通过分析代码,可以看到类中定义了一个静态的ThreadLocal常量对象,此ThreadLocal泛型是String类型。类包含了三个方法,都是在对当前的静态常量的ThreadLocal做操作。其中的三个方法都很简单,
- setDataSourceName()方法用于将当前数据源名称设置给ThreadLocal
- getDataSourceName()方法用于获取当前存在ThreadLocal中的数据源名称
- clearDataSourceName()方法用于清除当前存在ThreadLocal中的数据源名称
看了代码,我们来说书为什么需要这个类, 而且这个类非常重要。这个是考虑到多线程环境下数据源切换的问题。在多线程环境下,如果多个线程同时访问同一个方法,并且每个线程都使用不同的数据源,那么就需要对每个线程下的每个数据源进行动态切换。使用ThreadLocal的意义就是,用ThreadLocal来存取当前线程要使用的数据源名称。ThreadLocal是一种线程本地存储机制,它可以为每个线程提供一个独立的变量副本,是每个线程都可以独立操作自己的变量副本,而不会影响其他线程中的变量副本。所以我们在多数据源切换到时候,每个线程就能通过这个ThreadLocal中的变量副本得知当前线程所需要使用的是哪个数据源,而避免了多个线程同时调用一个方法访问不同数据源的时候导致的数据源混乱。同时,使用ThreadLocal也可以避免内存泄漏。如果对ThreadLocal 还不太熟悉的兄弟们,可以先搜索学习ThreadLocal的相关知识。
总体来说,上面方法在我们多数据源切换的时候主要负责:
- 每一个线程调用Mapper方法前先使用setDataSourceName()确认当前线程调用的数据源。
- 在真实调用的时候通过getDataSourceName()获取数据源名称得到数据源获取链接,
- 在使用完毕后通过clearDataSourceName()清除ThreadLocal中数据源名称保证安全释放。
2. 创建DynamicDataSource类
如果上个类是实现基础,这个类就是实现的核心。该类需要继承AbstractRoutingDataSource
我们来看看这个类的内容:
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContext.getDataSourceName();
}
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
super.setDefaultTargetDataSource(defaultTargetDataSource);
super.setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
}
}
该类就是我们在springboot中要使用到的多数据源。它在初始化的时候需要传入一个默认的主要数据源(defaultTargetDataSource),以及需要的其他数据源的集合(targetDataSources)。
要知道它怎么使用,我们还是要看它的父类AbstractRoutingDataSource。我们来简单看看他的几个主要参数和方法主要方法。(*代码并不完整,只是选了必须的几个方法和参数)
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
@Nullable
private Map<Object, Object> targetDataSources;
@Nullable
private Object defaultTargetDataSource;
@Nullable
private Map<Object, DataSource> resolvedDataSources;
@Nullable
private DataSource resolvedDefaultDataSource;
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
this.targetDataSources = targetDataSources;
}
public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
this.defaultTargetDataSource = defaultTargetDataSource;
}
@Override
public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
} else {
this.resolvedDataSources = new HashMap(this.targetDataSources.size());
this.targetDataSources.forEach((key, value) -> {
Object lookupKey = this.resolveSpecifiedLookupKey(key);
DataSource dataSource = this.resolveSpecifiedDataSource(value);
this.resolvedDataSources.put(lookupKey, dataSource);
});
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
}
}
}
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = this.determineCurrentLookupKey();
DataSource 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 + "]");
} else {
return dataSource;
}
}
@Nullable
protected abstract Object determineCurrentLookupKey();
}
首先,AbstractRoutingDataSource类继承了AbstractDataSource,而AbstractDataSource又实现了javax.sql的DataSource,DataSource类就是一个标准的获取数据库连接的工厂,他是一个标准,所有常见的数据库连接池都实现了这个接口(C3P0, DBCP, Hikari,Druid等)。所以AbstractRoutingDataSource也是这样的一个获取数据库链接的类。我们先看看他的参数:
- defaultTargetDataSource: 标识了项目默认的数据库连接
- targetDataSources:项目需要的其他数据库连接
- resolvedDefaultDataSource: 通过defaultTargetDataSource得来的真正使用的默认数据源。
- resolvedDataSources: 通过targetDataSources得来的真正使用的其他数据源。
AbstractRoutingDataSource类又实现了一个InitializingBean的接口。该接口只有一个抽象方法需要被AbstractRoutingDataSource实现,就是afterPropertiesSet方法。
@Override
public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
} else {
this.resolvedDataSources = new HashMap(this.targetDataSources.size());
this.targetDataSources.forEach((key, value) -> {
Object lookupKey = this.resolveSpecifiedLookupKey(key);
DataSource dataSource = this.resolveSpecifiedDataSource(value);
this.resolvedDataSources.put(lookupKey, dataSource);
});
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
}
}
}
afterPropertiesSet方法是初始化bean的时候就会执行的。在这个方法里面,我们能看到他将targetDataSources和defaultTargetDataSource转化成resloved数据源的过程。就意味着,当我们初始化了我们的动态数据源对象的时候,我们需要的所有数据源就再此准备妥当了。
重点来了,我们看看determineTargetDataSource()的方法,这个方法会在所有需要获取数据源的地方调用,例如Mybatis执行mapper中的增删改查的时候,或者jdbcTemplate调用数据库连接方法的时候。它的主要作用就是告诉project当前我们需要使用哪个数据源。其中有个determineCurrentLookupKey()方法,是用来确定数据源名称的,我们通过这个方法得到数据源,并在resolveDataSources中获取数据源,如果没有,并且获得的数据源名称也是空的,则返回默认的数据源。而这个determineCurrentLookupKey()需要我们在实现类中实现。这个实现类就是我们上面提到的DynamicDataSource
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
//获取当前数据源名称,这个方法需要在实现类中自行实现
Object lookupKey = this.determineCurrentLookupKey();
//通过数据源名称从resolveDataSources中得到数据源
DataSource 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 + "]");
} else {
return dataSource;
}
}
@Nullable
protected abstract Object determineCurrentLookupKey();
到最后我们再来看看DynamicDataSource就清楚了。
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContext.getDataSourceName();
}
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
super.setDefaultTargetDataSource(defaultTargetDataSource);
super.setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
}
}
方法初始化的时候就把配置好的默认数据源和其他数据源的集合赋值好。然后调用了afterPropertiesSet()方法来将他们转化为resolved的数据源。(或者将Bean交给Spring管理,让改动态数据源的bean在构建的时候自行调用afterPropertiesSet()方法)
然后在需要拿数据库连接的时候,系统会调用父类AbstractRoutingDataSource的determineTargetDataSource()方法,而该方法获取数据库连接名称的调用又在子类中实现。子类中实现方法是determineCurrentLookupKey(),他就是直接从ThreadLocal中获取到的线程名。
3. 初始化各个数据源
DynamicDataSource初始化的时候需要两个参数,就是默认数据源和其他数据源的集合。
这一步其实比较简单了。直接做一个Configuration类(maybe叫DynamicDataConfig类)该类来读取yml中配置的数据源信息并且把这些信息转换为targetDataSources和defalutTargetDataSource,并且然后通过这两个参数初始化一个DynamicDataSource装配到Bean里面就OK了。这部分不再赘述,yml相关内容如下:
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://****:3306/test
username: root
password: root
mutil-datasource:
connection:
- dbName: db-test1
dbDriver: com.microsoft.sqlserver.jdbc.SQLServerDriver
dbUrl: jdbc:sqlserver://****:1433;DatabaseName=db_test1
dbUsername: root
dbPassword: root
- dbName: db-test2
dbDriver: com.sybase.jdbc4.jdbc.SybDriver
dbUrl: jdbc:sybase:Tds:****:5000/db_test2
dbUsername: root
dbPassword: root
4. 使用并切换数据源
@Service
public class TestServiceImpl implements TestService {
@Autowired
private TestMapper testMapper;
@Override
public void test(User user) {
testMapper.insert(user);
}
@Override
public void test1(User user) {
DynamicDataSourceContext.setDataSourceName("db-test1");
testMapper.updateById(user);
DynamicDataSourceContext.clearDataSourceName()
}
@Override
public void test2(User user) {
DynamicDataSourceContext.setDataSourceName("db-test2");
testMapper.updateById(user);
DynamicDataSourceContext.clearDataSourceName()
}
}
我们在调用Mapper之前,使用了DynamicDataSourceContext类去设置了一个数据源名称。这样,当前线程的ThreadLocal中的key就是数据源名称了。然后在执行mapper的时候会隐式的调用determineTargetDataSource()方法。这样就能将数据源名称相关的数据源获取到,并获取到它的连接。当使用完毕后,再调用DynamicDataSourceContext将ThreadLocal的key给remove掉。
以上就是一个基本动态数据源的配置和讲解。在之后我们会引入更多的需求,例如最开始说的使用切面技术和注解来动态切换而无需手动设置。