What problems can multiple data sources solve

The scene of a

This is a real project requirement that I have experienced, and it is estimated that many people have experienced similar situation. The process of the project is to access SMS, and the SMS provider provides two solutions.

  1. Directly call the interface they provide, this common I experience behind some SMS providers are basically doing so
  2. Direct operation of their database (strange thing is our boss let me use this, at that time just come out of work I can’t remember what implementation. Now it seems appropriate to implement the following method 1)

Scenario 2

This is the amount of data in our project, the basic experience of read and write separation of the following implementation method 2 is suitable

Implementation Method 1

This implementation method in my opinion, whether the implementation or principle is relatively easy to understand (if you have seen mybatis source code, you can read my previous article), just use @mapperscan

Take a look at the implementation code

configuration

Spring. The datasource. Kiss. JDBC - url = JDBC: mysql: / / 127.0.0.1:3306 / kiss spring. The datasource. Kiss. The username = root spring.datasource.kiss.password=qwer1234 spring.datasource.kiss.driver-class-name=com.mysql.cj.jdbc.Driver Spring. The datasource. CRM. JDBC - url = JDBC: mysql: / / 127.0.0.1:3306 / CRM spring. The datasource. CRM. The username = root spring.datasource.crm.password=qwer1234 spring.datasource.crm.driver-class-name=com.mysql.cj.jdbc.DriverCopy the code

Two MyBatis configuration classes

@Configuration
// The key is the @mapperscan annotation
BasePackages basePackages basePackages basePackages basePackages
//sqlSessionTemplateRef is the same sqlSessionTemplateRef that you use to proxy the mapper interface in the package
SqlSessionFactoryRef is the same as sqlSessionTemplateRef. If you look at mybatis, you will see that sqlSessionTemplate is created by sqlSessionFactory
@MapperScan(basePackages="com.kiss.mxb.mapper001",sqlSessionTemplateRef="test1SqlSessionTemplate")
public class MybatisConfig001 {
	
	// Create a data source register with the Spring-IOC container set to the primary class
	@Bean(name = "test1DataSource")
	// The autowage property value must start with spring. DataSource
    @ConfigurationProperties(prefix = "spring.datasource.kiss")
    @Primary
    public DataSource testDataSource(a) {
        return DataSourceBuilder.create().build();
    }
	
	@Bean(name = "test1SqlSessionFactory")
    @Primary
    public SqlSessionFactory testSqlSessionFactory(@Qualifier("test1DataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper001/*.xml"));
        return bean.getObject();
    }
    
    // object creation
    @Bean(name = "test1TransactionManager")
    @Primary
    public DataSourceTransactionManager testTransactionManager(@Qualifier("test1DataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
    
    //SqlSessionTemplate registration is the bean we use
    @Bean(name = "test1SqlSessionTemplate")
    @Primary
    public SqlSessionTemplate testSqlSessionTemplate(@Qualifier("test1SqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
        return newSqlSessionTemplate(sqlSessionFactory); }}Copy the code
// This is similar to the above
@Configuration
@MapperScan(basePackages="com.kiss.mxb.mapper002",sqlSessionTemplateRef="test2SqlSessionTemplate")
public class MybatisConfig002 {
	
	@Bean(name = "test2DataSource")
    @ConfigurationProperties(prefix = "spring.datasource.crm")
    public DataSource testDataSource(a) {
        return DataSourceBuilder.create().build();
    }
	
	@Bean(name = "test2SqlSessionFactory")
    public SqlSessionFactory testSqlSessionFactory(@Qualifier("test2DataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper002/*.xml"));
        return bean.getObject();
    }

    @Bean(name = "test2TransactionManager")
    public DataSourceTransactionManager testTransactionManager(@Qualifier("test2DataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean(name = "test2SqlSessionTemplate")
    public SqlSessionTemplate testSqlSessionTemplate(@Qualifier("test2SqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
        return newSqlSessionTemplate(sqlSessionFactory); }}Copy the code

Implementation Method 2

This approach is implemented using AOP, with the code explained laterCopy the code
public class DataSourceHolder {
	
	// Which data source is needed to save the current thread
   private static final ThreadLocal<DBTypeEnum> contextHolder = new ThreadLocal<>();
	 
 
    public static void set(DBTypeEnum dbType) {
        contextHolder.set(dbType);
    }
 
    public static DBTypeEnum get(a) {
        return contextHolder.get();
    }
 
    public static void master(a) {
        set(DBTypeEnum.MASTER);
        System.out.println("Switch to master");
    }
 
    public static void slave(a) {
        set(DBTypeEnum.SLAVE);
        System.out.println("Switch to slave"); }}Copy the code
// Configure two data sources
@Configuration
public class DataSourceConfig {
	
    @Primary
    @Bean
    @ConfigurationProperties("spring.datasource.kiss")
    public DataSource kissDataSource(a) {
        return DataSourceBuilder.create().build();
    }
    
    @Bean
    @ConfigurationProperties("spring.datasource.crm")
    public DataSource crmDataSource(a) {
        returnDataSourceBuilder.create().build(); }}Copy the code
/ / AbstractRoutingDataSource parent class for AbstractDataSource top interface to the DataSource
/ / if you want to understand the principle of multiple source, first of all, I believe you know the DataSource is a top level interface, the realization of the different data sources, such as: HikariDataSource, BasicDataSource and so on
//AbstractDataSource is one of them, but what's different about it? We'll talk about it later
public class CustomRoutingDataSource extends AbstractRoutingDataSource  {
    
    // This is the key code that must be implemented to determine which data source should be used
	@Override
	protected Object determineCurrentLookupKey(a) {
        returnDataSourceHolder.get(); }}Copy the code
Override the sqlSessionFactoryBean method of the MybatisAutoConfiguration class// You may be relieved that we created two sqlsessionFactories ourselves
// The sqlSessionFactory is created by MybatisAutoConfiguration
@Configuration
@AutoConfigureAfter({DataSourceConfig.class})
public class MyMybatisAutoConfiguration extends MybatisAutoConfiguration {

    public MyMybatisAutoConfiguration(MybatisProperties properties, ObjectProvider
       
         interceptorsProvider, ObjectProvider
        
          typeHandlersProvider, ObjectProvider
         
           languageDriversProvider, ResourceLoader resourceLoader, ObjectProvider
          
            databaseIdProvider, ObjectProvider
           
            > configurationCustomizersProvider)
           
          
         []>
        []>
       []> {
    	super(properties, interceptorsProvider, typeHandlersProvider, languageDriversProvider, resourceLoader,
    			databaseIdProvider, configurationCustomizersProvider);
    	// TODO Auto-generated constructor stub
    }
    
    
     @Resource(name = "kissDataSource")
     private DataSource kissDataSource;
     @Resource(name = "crmDataSource")
     private DataSource crmDataSource;
    
    
    
     @Bean(name = "sqlSessionFactory")
     public SqlSessionFactory sqlSessionFactoryBean(a) throws Exception {
        // Rewrote the sqlSessionFactoryBean to use our dataSource implementation
        / / see customRoutingDataSource ()
         return super.sqlSessionFactory(customRoutingDataSource());
     }
     
     // The key is this method
     @Bean(name = "customRoutingDataSource")
     public CustomRoutingDataSource customRoutingDataSource(a){
        / / create a map for your configuration data key value is determineCurrentLookupKey () method may return
         Map<Object, Object> targetDataSources = new HashMap<Object, Object>(2);
         targetDataSources.put(DBTypeEnum.MASTER,kissDataSource);
         targetDataSources.put(DBTypeEnum.SLAVE,crmDataSource);
         / / create our implementation AbstractRoutingDataSource data sources
         CustomRoutingDataSource customRoutingDataSource = new CustomRoutingDataSource();
         // Set the target data source
         customRoutingDataSource.setTargetDataSources(targetDataSources);
         // Set the default data source
         customRoutingDataSource.setDefaultTargetDataSource(customRoutingDataSource);
         returncustomRoutingDataSource; }}Copy the code
/ / aop aspects
@Aspect
@Component
public class DataSourceAop {

    / / com. Kiss. MXB. The annotation. Kiss com. Kiss. MXB. The annotation. Crm is my custom annotations, you can also specify what method at the beginning of, aop is this stuff, not much said, the expression or a lot
    @Pointcut("@annotation(com.kiss.mxb.annotation.Kiss)")
    public void KissPointcut(a) {}@Pointcut("@annotation(com.kiss.mxb.annotation.Crm)")
    public void crmPointcut(a) {}// when a method annotated by @kiss is executed
    @Before("KissPointcut()")
    public void kiss(a) {
    	DataSourceHolder.master();
    }
    
    // when a method annotated by @ciss is executed
    @Before("crmPointcut()")
    public void crm(a) { DataSourceHolder.slave(); }}Copy the code
The implementation is done, the test class is not writtenCopy the code

The principle of

In fact, there is nothing to say about the principle

Configurtion: / / sqlSessionFactory: / / environment. DataSource: / / But sqlSessionFactory configurtion is created when I was in the ioc container startup, so the dataSource is fixed

To be honest, before I explain this, you’d better know how mybatis is integrated by Spring, and how our Mapper interface is instantiated. If you don’t understand, I suggest you go to see my previous mybatis source code that several articles

The mapper interface is instantiated using the dataSource interface

Methods a

This is easy to understand, we specify different scan bread under the Mapper layer interface using different data sources using @mapperscan. The sqlSessionTemplateRef parameter is used to determine the data source before the Mapper layer interface is proxyed. The mapper interface of different packages uses different data sources, which is very much in line with the scenario I mentioned above

Way 2

This is a little mean, the problem is that we now create sqlSessionFactory is used we implement DataSource is AbstractRoutingDataSource

AbstractRoutingDataSource this actually can see something different from the name of the local routing

So let’s start with the way I thought about it when I looked at the code

  1. First OF all, I need to look at the Connection created by the data source that mybatis uses when executing because you need to use JDBC Connection to operate the data source
  2. And then look at what happens before this or the connection, is it a data source switch implemented

Look at the code

//org.apache.ibatis.transaction.managed.ManagedTransaction.openConnection()
// Take a look at the method
protected void openConnection(a) throws SQLException {
    if (log.isDebugEnabled()) {
      log.debug("Opening JDBC Connection");
    }
    // Get the connection based on the data source
    / / we now data source for AbstractRoutingDataSource
    / / then go see AbstractRoutingDataSource getConnection ();
    this.connection = this.dataSource.getConnection();
    if (this.level ! =null) {
      this.connection.setTransactionIsolation(this.level.getLevel()); }}Copy the code
@Override
public Connection getConnection(a) throws SQLException {
    / / key code determineTargetDataSource ()
    This method returns a concrete executable data source and then calls the data source getConnection().
    / / which can be learned that determineTargetDataSource () is our key dynamic data source for the forehead
	return determineTargetDataSource().getConnection();
}
Copy the code
protected DataSource determineTargetDataSource(a) {
	Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
	// Remember that the method we overwrite gets the flag of the data source to be used by the currently executing method
	Object lookupKey = determineCurrentLookupKey();
	AfterPropertiesSet () = resolvedDataSources ();
	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 + "]");
	}
	// Return the switchable DataSource
	return dataSource;
}

// Initialize method
@Override
public void afterPropertiesSet(a) {
	if (this.targetDataSources == null) {
		throw new IllegalArgumentException("Property 'targetDataSources' is required");
	}
	this.resolvedDataSources = new HashMap<>(this.targetDataSources.size());
	// Put our targetDataSources in resolvedDataSources
	this.targetDataSources.forEach((key, value) -> {
		Object lookupKey = resolveSpecifiedLookupKey(key);
		DataSource dataSource = resolveSpecifiedDataSource(value);
		this.resolvedDataSources.put(lookupKey, dataSource);
	});
	if (this.defaultTargetDataSource ! =null) {
		this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource); }}Copy the code