Fork me on GitHub

SpringBoot多数据源配置

前言

之前研究了下SpringBoot下多数据源的配置处理,感觉蛮有意思的,今天特地总结分享下。

我们知道,当一些项目较大时,有可能出现分库等技术操作,这时候就有可能需要使用多数据源了,为保证程序SQL能在多个数据源之间来回切换并正常执行,就需要对数据源代码上进行一些处理,这里我已SpringBoot下的配置为例。

正文

如何配置是简单的,但是要了解如何配置,我们应该下些功夫。

在spring-jdbc这个jar包里,我们可以找到这个抽象类AbstractRoutingDataSource.class,这个类是处理多数据源配置的关键类,我们来看一下。

upload successful

这个类的主要作用是是的我们可以切换数据源key以切换数据源,key值的切换一般通过线程Context实现。

targetDataSources: 是一个Map集合,用于存放多个数据源。
resolvedDataSources:dataSource的Map集合,用于确定多个数据源。
defaultTargetDataSource:默认使用的数据源。
lenientFallback:可以用来回滚到原来的数据源的设置。
dataSourceLookup:数据源循环查看实现,有多个实现。
resolvedDefaultDataSource:确定的要使用哪一个数据源。

我们来看一些相关代码。

先看属性设置完成后执行的这个方法。

upload successful

可以看到,resolvedDataSources从targetDataSources拿到值并转换为DataSource集合,resolvedDefaultDataSource会对defaultTargetDataSource进行转化处理。

再看这个决定使用哪个数据源的方法。

upload successful

可以看到会先拿到当前的LookupKey,即当前要使用的数据源的key,拿到key后在尝试拿到当前key对应的数据源,如果没有数据源并且设置了回滚属性,会继续使用默认的数据源而不切换,如果没有设置回滚又拿不到数据源,dataSource==null,那么就会抛出异常,如果拿到数据源了,dataSource!=null,那么就会进行数据源的切换,返回一个切换后的数据源。

这儿lenientFallback(仁慈回滚)的作用,如果设置为false,拿不到数据源会出错,便于我们分析问题。

再看下 determineCurrentLookupKey 这个方法,就是获取要使用的数据源的key,是个抽象方法,需要我们进行实现。(具体什么时候切换数据源,就改变key值,我们一般使用线程绑定的上下文来对key值进行控制)

upload successful

我们再来看下把把我们的对象转为数据源对象的这个方法。

upload successful

可以看到如果传入的是String,会使用DataSourceLookup去获取数据源,默认使用JndiDataSourceLookup。

DataSourceLookup有4种实现,可以看下。

upload successful

  1. BeanFactoryDataSourceLookup是从SpringBean里获取DataSource。

    upload successful

  2. JndiDataSourceLookup是通过JndiTemplate获取DataSource。

    upload successful

  3. MapDataSourceLookup可以自定义DataSourceMap,然后根据Key值获取。下图中的dataSources就是我们可以提前主动set进去的数据。

    upload successful

  4. SingleDataSourceLookup指的单一数据源,一般很少使用。这个dataSource我们也是可以主动设置的。

    upload successful

可以看到,我们有多种方式去放置数据源,targetDataSources这个Map集合可以直接放置数据源集合,也可以放置数据源bean名字(但需要指定DataSourceLookup为BeanFactoryDataSourceLookup去解析),也可以放置自定义数据源集合的key(但需要指定DataSourceLookup为MapDataSourceLookup去解析)等等。

因此我们要实现多数据源,需要对AbstractRoutingDataSource进行实现。

多数据源配置

新建DynamicDataSource类,内容如下:

1
2
3
4
5
6
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DatabaseContextHolder.getDatabaseType();
}
}

看一下lookupKey的实现,我们新创建一个DatabaseContextHolder类,用一个ThreadLocal来对当前正在使用的数据源key进行管理。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class DatabaseContextHolder {
/**
* 使用ThreadLocal管理数据源
*/
private static final ThreadLocal<DatabaseType> contextHolder = new ThreadLocal<>();
/**
* 当前可以使用的所有数据源key值
*/
public static Set<DatabaseType> databaseTypes = new HashSet<>();
/**
* 设置数据源类型
* @param type
*/
public static void setDatabaseType(DatabaseType type) {
contextHolder.set(type);
}
/**
* 获取数据源类型
* @return
*/
public static DatabaseType getDatabaseType() {
return contextHolder.get();
}
/**
* 清除数据源类型
*/
public static void clearDatabaseType(){
contextHolder.remove();
}
}

databaseTypes 这个set我们后面可以用来判断是否有此数据源。

DatabaseType枚举如下(用静态String等也可以代替):

1
2
3
public enum DatabaseType {
MASTER,SLAVE
}

可以看到有两个数据源key值,MASTER和SLAVE。

然后我们要使多数据源生效,需要配置多数据源,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Configuration
@MapperScan(basePackages = "com.zwt.frameworkdatasource.mapper")
public class DynamicDataSourceConfig {

/**
* 主数据源
* @return
*/
@Bean(name = "masterDataSource")
@Primary
@ConfigurationProperties(prefix = "spring.datasource.multiple.master-datasource")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}

/**
* 从数据源
* @return
*/
@Bean(name = "slaveDataSource")
@ConfigurationProperties(prefix = "spring.datasource.multiple.slave-datasource")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().build();
}
......
}

配置文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
spring.datasource.multiple.master-datasource.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.multiple.master-datasource.jdbcUrl=jdbc:mysql://localhost:3306/test1?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
spring.datasource.multiple.master-datasource.username=root
spring.datasource.multiple.master-datasource.password=root
spring.datasource.multiple.master-datasource.initialSize=5
spring.datasource.multiple.master-datasource.minIdle=1
spring.datasource.multiple.master-datasource.maxActive=50

spring.datasource.multiple.slave-datasource.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.multiple.slave-datasource.jdbcUrl=jdbc:mysql://localhost:3306/test2?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
spring.datasource.multiple.slave-datasource.username=root
spring.datasource.multiple.slave-datasource.password=root
spring.datasource.multiple.slave-datasource.initialSize=5
spring.datasource.multiple.slave-datasource.minIdle=1
spring.datasource.multiple.slave-datasource.maxActive=50

我们说下这个Config类。
masterDataSource和slaveDataSource很好理解,就是获取配置文件属性生成相应的Bean,这儿要注意@Primary注解,这个注解指定默认使用哪个Bean,因为DataSource有两个实现,master和slave,如果不指定,Spring会不知道选用哪一个。

要使用多数据源,就需要对两个数据源进行管理,我们在Config这个类里创建一个返回DynamicDataSource的方法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 动态数据源
* @param masterDataSource
* @param slaveDataSource
* @return
*/
@Bean
public DynamicDataSource dataSource(@Qualifier("masterDataSource") DataSource masterDataSource,@Qualifier("slaveDataSource") DataSource slaveDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DatabaseType.MASTER, masterDataSource);
targetDataSources.put(DatabaseType.SLAVE, slaveDataSource);

DatabaseContextHolder.databaseTypes.add(DatabaseType.MASTER);
DatabaseContextHolder.databaseTypes.add(DatabaseType.SLAVE);

DynamicDataSource dataSource = new DynamicDataSource();
dataSource.setTargetDataSources(targetDataSources);
dataSource.setDefaultTargetDataSource(masterDataSource);
return dataSource;
}

可以看到我们把MASTER数据源和SLAVE数据源放到了targetDataSources 里面,同时在
DatabaseContextHolder的databaseTypes里面保存一份key值,用于一些处理。整体上这个类还是比较好理解的。

如何让程序执行SQL使用使用动态数据源呢?这时候就要处理SqlSessionFactory了,我们创建获取SqlSessionFactory的Bean的方法,传入动态数据源获取SqlSessionFactory,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 多数据源切换关键类
* @param dynamicDataSource
* @return
* @throws Exception
*/
@Bean
public SqlSessionFactory sqlSessionFactory(DynamicDataSource dynamicDataSource) throws Exception{
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dynamicDataSource);
sqlSessionFactoryBean.setTypeAliasesPackage("com.zwt.frameworkdatasource.model");
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:sqlmap/mapper/*.xml"));
return sqlSessionFactoryBean.getObject();
}

这个类是是数据源切换的关键类。

为了管理支持多数据源事务,我们的事务Bean也是要传入动态数据源的,如下:

1
2
3
4
5
6
7
8
9
/**
* 事务管理
* @param dynamicDataSource
* @return
*/
@Bean
public PlatformTransactionManager transactionManager(DynamicDataSource dynamicDataSource){
return new DataSourceTransactionManager(dynamicDataSource);
}

这样随着数据源的切换,事务也是切换后的数据源的。

如何切换数据源?其实只要控制DatabaseContextHolder里面ThreadLocal的值就可以控制数据源切换,但是我们想切换数据源时,总不能每个方法都在方法开始前设置,在结束前清除吧,那样太麻烦了。

我们可以借助Aspect切面来处理这个问题。
我们定义一个TargetDataSource注解,只要标注这个注解,并指定数据源key便可以进行数据源切换,这个注解一定是作用在方法上的,如下:

1
2
3
4
5
6
7
8
9
10
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {
/**
* 数据源key
* @return
*/
DatabaseType database() default DatabaseType.MASTER;
}

切面的话,只要检测到这个方法上有这个注解,那就拿到注解里的数据源key进行切换,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Aspect
@Order(-1)//保证在@Transactional之前执行
@Component
public class DynamicDataSourceAspect {
private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class);

/**
* 改变数据源
* @param joinPoint
* @param targetDataSource
*/
@Before("@annotation(targetDataSource)")
public void changeDataSource(JoinPoint joinPoint, TargetDataSource targetDataSource) {
DatabaseType type = targetDataSource.database();
if(DatabaseContextHolder.databaseTypes.contains(type)){
logger.info("使用数据源:"+ type);
DatabaseContextHolder.setDatabaseType(type);
}else{
logger.error("未配置数据源,使用默认数据源:MASTER");
DatabaseContextHolder.setDatabaseType(DatabaseType.MASTER);
}
}


/**
* 清除数据源
* @param joinPoint
* @param targetDataSource
*/
@After("@annotation(targetDataSource)")
public void clearDataSource(JoinPoint joinPoint, TargetDataSource targetDataSource) {
logger.info("清除数据源 " + targetDataSource.database());
DatabaseContextHolder.clearDatabaseType();
}
}

@Order注解设置为-1保证此过程在事务之前执行,然后在执行事务。
@Before注解表示在在动作之前执行,我们设置为在有此注解的方法之前执行changeDataSource方法,这个方法对数据源进行切换,@After注解表示方法执行完之后要处理的事情,这儿我们用clearDataSource方法来清除使用过的数据源信息。

讲到这儿,SpringBoot的多数据源配置也基本完成了,我们进行必要的测试。

测试

创建一个Service类,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Service
public class TestServiceImpl implements TestService {

@Autowired
private ScoreMapper scoreMapper;
boolean flag = false;
@Override
@TargetDataSource(database = DatabaseType.MASTER)
@Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
public void doSomething1() {
ScoreModel scoreModel = new ScoreModel();
scoreModel.setId(1234);
scoreMapper.insert(scoreModel);
if(!flag){
throw new RuntimeException("出现异常");
}
}
@Override
@TargetDataSource(database = DatabaseType.SLAVE)
@Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
public void doSomething2() {
ScoreModel scoreModel = new ScoreModel();
scoreModel.setId(12345);
scoreMapper.insert(scoreModel);
if(flag){
throw new RuntimeException("出现异常");
}
}
}

注:这儿略掉了Score表和Mapper文件的创建。

上面的TestServiceImpl,我们应该期望的结果是 doSomething1执行成功入库test1,doSomething2执行失败不能入库test2(因为抛出异常事务要回滚),我们看下执行结果:

新建Test,执行我们的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@RunWith(SpringRunner.class)
@SpringBootTest
public class FrameworkDatasourceApplicationTests {

@Autowired
TestService testService;
@Test
public void contextLoads() {
try{
testService.doSomething1();
}catch(Exception e){
System.out.println(e);
}
try{
testService.doSomething2();
}catch(Exception e){
System.out.println(e);
}
}
}

可以看到如下输出:

upload successful

查看数据库。

upload successful

upload successful

符合我们期望值。我们把数据库数据清除后,把TestServiceImpl里的flag设置为false。
执行刚才的测试方法,结果如下:

upload successful

upload successful

upload successful

可以看到也符合我们的期望结果,成功完成了数据源切换及事务支持。

总结

通过对SpringBoot多数据源的配置和理解,了解了多数据源的配置和使用,及多数据源实现的一些简单原理,也是蛮不错的一次学习过程。

本节源代码详见: framework-datasource




-------------文章结束啦 ~\(≧▽≦)/~ 感谢您的阅读-------------

SakuraTears wechat
扫一扫关注我的公众号
您的支持就是我创作的动力!
0%