转载

springboot+mybatis多数据源实现原理

这个个我经历的真实的项目需求,估计很多人都经历过了类似的情况,事情过程:项目中要接入短信,短息提供方提供了两种方案.

  1. 直接调用他们提供的接口,这个常见的我后面经历的一些短信提供方基本都是这样搞的
  2. 直接操作他们数据库(奇葩的是我们老大让我使用这个,当时刚出来上班我都想不起来啥实现的了.现在看来很适合下面的 实现方式一 )

场景二

这个就是我们项目数据量上来了,基本都会经历的 读写分离 下面的 实现方式二 很适合

实现方法一

这个实现方法在我看来不管是实现还是原理都是比较通俗易懂的( 假如你看过mybatis源码 ,没看过可以翻翻我以前的文章),只是使用了 @MapperScan

直接看下实现代码

配置

spring.datasource.kiss.jdbc-url=jdbc:mysql://127.0.0.1:3306/kiss
spring.datasource.kiss.username=root
spring.datasource.kiss.password=qwer1234
spring.datasource.kiss.driver-class-name=com.mysql.cj.jdbc.Driver


spring.datasource.crm.jdbc-url=jdbc:mysql://127.0.0.1:3306/crm
spring.datasource.crm.username=root
spring.datasource.crm.password=qwer1234
spring.datasource.crm.driver-class-name=com.mysql.cj.jdbc.Driver

复制代码

两个mybatis配置类

@Configuration
//关键就是 这个 @MapperScan注解 
//basePackages这个参数我们经常使用就是指定扫面的包
//sqlSessionTemplateRef 就是指定你扫面的包里面的 mapper接口 做代理的时候 是使用的那个 sqlSessionTemplateRef
//这个地方说下使用sqlSessionFactoryRef这个参数和sqlSessionTemplateRef是一样的,假如你看了mybatis'源码你会发现 sqlSessionTemplate是sqlSessionFactory创建的
@MapperScan(basePackages="com.kiss.mxb.mapper001",sqlSessionTemplateRef="test1SqlSessionTemplate")
public class MybatisConfig001 {
	
	//创建数据源注册到 spring-ioc容器 设置为主类 
	@Bean(name = "test1DataSource")
	//这个自动装配属性值的,但是必须是 spring.dataSource 开头
    @ConfigurationProperties(prefix = "spring.datasource.kiss")
    @Primary
    public DataSource testDataSource() {
        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();
    }
    
    //事物创建
    @Bean(name = "test1TransactionManager")
    @Primary
    public DataSourceTransactionManager testTransactionManager(@Qualifier("test1DataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
    
    //SqlSessionTemplate注册 就是我们使用的 bean
    @Bean(name = "test1SqlSessionTemplate")
    @Primary
    public SqlSessionTemplate testSqlSessionTemplate(@Qualifier("test1SqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

}
复制代码
//这个和上面的类似不介绍了
@Configuration
@MapperScan(basePackages="com.kiss.mxb.mapper002",sqlSessionTemplateRef="test2SqlSessionTemplate")
public class MybatisConfig002 {
	
	@Bean(name = "test2DataSource")
    @ConfigurationProperties(prefix = "spring.datasource.crm")
    public DataSource testDataSource() {
        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 new SqlSessionTemplate(sqlSessionFactory);
    }
	

}

复制代码

实现方法二

这种方式是使用aop实现的,先看代码,随后会解释原理
复制代码
public class DataSourceHolder {
	
	//保存当前线程的需要使用那个数据源
   private static final ThreadLocal<DBTypeEnum> contextHolder = new ThreadLocal<>();
	 
 
    public static void set(DBTypeEnum dbType) {
        contextHolder.set(dbType);
    }
 
    public static DBTypeEnum get() {
        return contextHolder.get();
    }
 
    public static void master() {
        set(DBTypeEnum.MASTER);
        System.out.println("切换到master");
    }
 
    public static void slave() {
        set(DBTypeEnum.SLAVE);
        System.out.println("切换到slave");
    }

}
复制代码
//配置两个数据源
@Configuration
public class DataSourceConfig {
	
    @Primary
    @Bean
    @ConfigurationProperties("spring.datasource.kiss")
    public DataSource kissDataSource() {
        return DataSourceBuilder.create().build();
    }
    
    @Bean
    @ConfigurationProperties("spring.datasource.crm")
    public DataSource crmDataSource() {
        return DataSourceBuilder.create().build();
    }

}
复制代码
//AbstractRoutingDataSource 的父类为AbstractDataSource 最顶层接口为 DataSource
//你要想搞懂多数据源的原理,首先我相信你是知道 DataSource 是个顶层接口,不同的数据源有不同的实现,例如:HikariDataSource,BasicDataSource之类
//AbstractDataSource也是其中一种,有啥不同呢,我们稍后会讲
public class CustomRoutingDataSource extends AbstractRoutingDataSource  {
    
    //这个是必须实现的,也是关键代码,判断当前该使用那个数据源的依据
	@Override
	protected Object determineCurrentLookupKey() {
        return DataSourceHolder.get();
	}

}

复制代码
重写MybatisAutoConfiguration类的sqlSessionFactoryBean方法
//方式一你可能会放心我们自己创建了2个sqlSessionFactory
//正常的项目中sqlSessionFactory是MybatisAutoConfiguration创建的,不懂的话可以看下我以前写的mybatis源码的文章
@Configuration
@AutoConfigureAfter({DataSourceConfig.class})
public class MyMybatisAutoConfiguration extends MybatisAutoConfiguration {

    public MyMybatisAutoConfiguration(MybatisProperties properties, ObjectProvider<Interceptor[]> interceptorsProvider,
    		ObjectProvider<TypeHandler[]> typeHandlersProvider,
    		ObjectProvider<LanguageDriver[]> languageDriversProvider, ResourceLoader resourceLoader,
    		ObjectProvider<DatabaseIdProvider> databaseIdProvider,
    		ObjectProvider<List<ConfigurationCustomizer>> 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() throws Exception {
        //重写了sqlSessionFactoryBean  使用我们自己实现的dataSource
        //请看customRoutingDataSource()
         return super.sqlSessionFactory(customRoutingDataSource());
     }
     
     //关键就是这个方法
     @Bean(name = "customRoutingDataSource")
     public CustomRoutingDataSource customRoutingDataSource(){
        //创建map存放你配置的数据源 key值为determineCurrentLookupKey() 方法可能返回的东西
         Map<Object, Object> targetDataSources = new HashMap<Object, Object>(2);
         targetDataSources.put(DBTypeEnum.MASTER,kissDataSource);
         targetDataSources.put(DBTypeEnum.SLAVE,crmDataSource);
         //创建我们实现AbstractRoutingDataSource的数据源  
         CustomRoutingDataSource customRoutingDataSource = new CustomRoutingDataSource();
         //设置目标数据源
         customRoutingDataSource.setTargetDataSources(targetDataSources);
         //设置默认数据源
         customRoutingDataSource.setDefaultTargetDataSource(customRoutingDataSource);
         return customRoutingDataSource;
     }
	
	

}
复制代码
//aop切面
@Aspect
@Component
public class DataSourceAop {

    //com.kiss.mxb.annotation.Kiss com.kiss.mxb.annotation.Crm 都是我自定义的注解,也可以指定什么方法开头的,这是aop的东西,不多说了,表达式还是很多的
    @Pointcut("@annotation(com.kiss.mxb.annotation.Kiss)")
    public void KissPointcut() {
    
    }
    
    @Pointcut("@annotation(com.kiss.mxb.annotation.Crm)")
    public void crmPointcut() {
    
    }
    
    //当被@kiss注释的方法执行 
    @Before("KissPointcut()")
    public void kiss() {
    	DataSourceHolder.master();
    }
    
    //当被@Ciss注释的方法执行 
    @Before("crmPointcut()")
    public void crm() {
    	DataSourceHolder.slave();
    }
}

复制代码
实现已经写了完了,测试类不写了
复制代码

原理

其实原理也没啥说的

sqlSessionFactory 创建 sqlSession 的过程中使用configurtion 中的environment.dataSource 创建了transaction , 但是 sqlSessionFactory.configurtion 是在 ioc容器启动的时候就创建了,所以说dataSource 是固定的

说实话我解释这个之前你最好要知道mybatis是如何被spring整合的,以及我们的mapper层接口是如何实例化的,假如不懂的话,我建议你去看看我前面讲的mybatis源码的那几篇文章

上面说到mapper接口实例化的的时候dataSource的固定的,那么如何做到数据源的切换呢

方式一

这个很容易理解,我们在使用 @MapperScan 指定了 不同的扫面包下面的 mapper层接口 使用不同的数据源.就是那个sqlSessionTemplateRef参数,所以说 方式一 是在mapper层接口被代理之前就确定了数据源,不同的包的mapper接口使用不同的数据源,非常符合我上面提到的 场景一 使用

方式二

这个就有点意思了,问题就在于我们现在创建的 sqlSessionFactory 是使用了我们实现的DataSource 就是AbstractRoutingDataSource

AbstractRoutingDataSource 这个其实从名字也能看出点不一样的地方 路由

先说先我当时看代码时候的结题思路

  1. 首先我要看看mybatis执行的时候吃使用的那个数据源创建的 Connection 因为你操作数据源不就得使用 jdbc的连接吗
  2. 然后看看这或者连接之前经历了什么,是实现的数据源切换

看代码

//org.apache.ibatis.transaction.managed.ManagedTransaction.openConnection()
//看下这个方法
protected void openConnection() throws SQLException {
    if (log.isDebugEnabled()) {
      log.debug("Opening JDBC Connection");
    }
    //根据数据源 获取 连接
    //我们现在的数据源为 AbstractRoutingDataSource
    //那就去看看AbstractRoutingDataSource.getConnection();
    this.connection = this.dataSource.getConnection();
    if (this.level != null) {
      this.connection.setTransactionIsolation(this.level.getLevel());
    }
  }
复制代码
@Override
public Connection getConnection() throws SQLException {
    //关键代码 determineTargetDataSource() 
    //啥意思呢  就是这个方法返回一个具体可执行的数据源 然后再去调用数据源的 getConnection()
    //由此可得知 determineTargetDataSource() 就是我们动态获取数据源的额关键
	return determineTargetDataSource().getConnection();
}
复制代码
protected DataSource determineTargetDataSource() {
	Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
	//记得这个方法吧 我们重写的方法,能获取当当前执行的方法 要使用的数据源的 标志
	Object lookupKey = determineCurrentLookupKey();
	//这里获取数据源  你可能会问 resolvedDataSources 是个啥 好吧看下下面的afterPropertiesSet()
	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 + "]");
	}
	//返回切换的DataSource
	return dataSource;
}

//初始化方法
@Override
public void afterPropertiesSet() {
	if (this.targetDataSources == null) {
		throw new IllegalArgumentException("Property 'targetDataSources' is required");
	}
	this.resolvedDataSources = new HashMap<>(this.targetDataSources.size());
	//把我们设置的 targetDataSources 放入 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);
	}
}
复制代码
原文  https://juejin.im/post/5e65fbed51882549122abda0
正文到此结束
Loading...