前言
之前研究了下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]()
- BeanFactoryDataSourceLookup是从SpringBean里获取DataSource。 ![upload successful]() 
 
- JndiDataSourceLookup是通过JndiTemplate获取DataSource。 ![upload successful]() 
 
- MapDataSourceLookup可以自定义DataSourceMap,然后根据Key值获取。下图中的dataSources就是我们可以提前主动set进去的数据。 ![upload successful]() 
 
- SingleDataSourceLookup指的单一数据源,一般很少使用。这个dataSource我们也是可以主动设置的。 ![upload successful]() 
 
可以看到,我们有多种方式去放置数据源,targetDataSources这个Map集合可以直接放置数据源集合,也可以放置数据源bean名字(但需要指定DataSourceLookup为BeanFactoryDataSourceLookup去解析),也可以放置自定义数据源集合的key(但需要指定DataSourceLookup为MapDataSourceLookup去解析)等等。
因此我们要实现多数据源,需要对AbstractRoutingDataSource进行实现。
多数据源配置
新建DynamicDataSource类,内容如下:
| 12
 3
 4
 5
 6
 
 | public class DynamicDataSource extends AbstractRoutingDataSource {@Override
 protected Object determineCurrentLookupKey() {
 return DatabaseContextHolder.getDatabaseType();
 }
 }
 
 | 
看一下lookupKey的实现,我们新创建一个DatabaseContextHolder类,用一个ThreadLocal来对当前正在使用的数据源key进行管理。如下:
| 12
 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等也可以代替):
| 12
 3
 
 | public enum DatabaseType {MASTER,SLAVE
 }
 
 | 
可以看到有两个数据源key值,MASTER和SLAVE。
然后我们要使多数据源生效,需要配置多数据源,如下:
| 12
 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();
 }
 ......
 }
 
 | 
配置文件内容如下:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 
 | spring.datasource.multiple.master-datasource.driverClassName=com.mysql.cj.jdbc.Driverspring.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的方法,如下:
| 12
 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,如下:
| 12
 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也是要传入动态数据源的,如下:
| 12
 3
 4
 5
 6
 7
 8
 9
 
 | 
 
 
 
 @Bean
 public PlatformTransactionManager transactionManager(DynamicDataSource dynamicDataSource){
 return new DataSourceTransactionManager(dynamicDataSource);
 }
 
 | 
这样随着数据源的切换,事务也是切换后的数据源的。
如何切换数据源?其实只要控制DatabaseContextHolder里面ThreadLocal的值就可以控制数据源切换,但是我们想切换数据源时,总不能每个方法都在方法开始前设置,在结束前清除吧,那样太麻烦了。
我们可以借助Aspect切面来处理这个问题。
我们定义一个TargetDataSource注解,只要标注这个注解,并指定数据源key便可以进行数据源切换,这个注解一定是作用在方法上的,如下:
| 12
 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进行切换,如下:
| 12
 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类,代码如下:
| 12
 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
 
 | @Servicepublic 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,执行我们的方法。
| 12
 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