前言
之前研究了下SpringBoot下多数据源的配置处理,感觉蛮有意思的,今天特地总结分享下。
我们知道,当一些项目较大时,有可能出现分库等技术操作,这时候就有可能需要使用多数据源了,为保证程序SQL能在多个数据源之间来回切换并正常执行,就需要对数据源代码上进行一些处理,这里我已SpringBoot下的配置为例。
正文
如何配置是简单的,但是要了解如何配置,我们应该下些功夫。
在spring-jdbc这个jar包里,我们可以找到这个抽象类AbstractRoutingDataSource.class,这个类是处理多数据源配置的关键类,我们来看一下。
这个类的主要作用是是的我们可以切换数据源key以切换数据源,key值的切换一般通过线程Context实现。
targetDataSources: 是一个Map集合,用于存放多个数据源。
resolvedDataSources:dataSource的Map集合,用于确定多个数据源。
defaultTargetDataSource:默认使用的数据源。
lenientFallback:可以用来回滚到原来的数据源的设置。
dataSourceLookup:数据源循环查看实现,有多个实现。
resolvedDefaultDataSource:确定的要使用哪一个数据源。
我们来看一些相关代码。
先看属性设置完成后执行的这个方法。
可以看到,resolvedDataSources从targetDataSources拿到值并转换为DataSource集合,resolvedDefaultDataSource会对defaultTargetDataSource进行转化处理。
再看这个决定使用哪个数据源的方法。
可以看到会先拿到当前的LookupKey,即当前要使用的数据源的key,拿到key后在尝试拿到当前key对应的数据源,如果没有数据源并且设置了回滚属性,会继续使用默认的数据源而不切换,如果没有设置回滚又拿不到数据源,dataSource==null,那么就会抛出异常,如果拿到数据源了,dataSource!=null,那么就会进行数据源的切换,返回一个切换后的数据源。
这儿lenientFallback(仁慈回滚)的作用,如果设置为false,拿不到数据源会出错,便于我们分析问题。
再看下 determineCurrentLookupKey 这个方法,就是获取要使用的数据源的key,是个抽象方法,需要我们进行实现。(具体什么时候切换数据源,就改变key值,我们一般使用线程绑定的上下文来对key值进行控制)
我们再来看下把把我们的对象转为数据源对象的这个方法。
可以看到如果传入的是String,会使用DataSourceLookup去获取数据源,默认使用JndiDataSourceLookup。
DataSourceLookup有4种实现,可以看下。
BeanFactoryDataSourceLookup是从SpringBean里获取DataSource。
JndiDataSourceLookup是通过JndiTemplate获取DataSource。
MapDataSourceLookup可以自定义DataSourceMap,然后根据Key值获取。下图中的dataSources就是我们可以提前主动set进去的数据。
SingleDataSourceLookup指的单一数据源,一般很少使用。这个dataSource我们也是可以主动设置的。
可以看到,我们有多种方式去放置数据源,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 {
private static final ThreadLocal<DatabaseType> contextHolder = new ThreadLocal<>();
public static Set<DatabaseType> databaseTypes = new HashSet<>();
public static void setDatabaseType(DatabaseType type) { contextHolder.set(type); }
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 {
@Bean(name = "masterDataSource") @Primary @ConfigurationProperties(prefix = "spring.datasource.multiple.master-datasource") public DataSource masterDataSource() { return DataSourceBuilder.create().build(); }
@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
|
@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
|
@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
|
@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 {
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) @Component public class DynamicDataSourceAspect { private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class);
@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); } }
@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); } } }
|
可以看到如下输出:
查看数据库。
符合我们期望值。我们把数据库数据清除后,把TestServiceImpl里的flag设置为false。
执行刚才的测试方法,结果如下:
可以看到也符合我们的期望结果,成功完成了数据源切换及事务支持。
总结
通过对SpringBoot多数据源的配置和理解,了解了多数据源的配置和使用,及多数据源实现的一些简单原理,也是蛮不错的一次学习过程。
本节源代码详见: framework-datasource