【多数据源】dynamic-datasource原理及使用

多数据源实现

如果只在意实现,直接看 dynamic-datasource【开源组件实现多数据源】🚩

多数据源的需求:

  1. 不同的业务分多个数据库场景,例如一个程序负责n个省份的db操作
  2. 一主多从的读写分离的场景(一主多从可以使用myBatis插件的方式实现)

注意:多数据源实现离不开Spring提供的AbstractRoutingDataSource类

AbstractRoutingDataSource类

AbstractRoutingDataSource部分成员变量

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
    @Nullable
    private Map<Object, Object> targetDataSources;
    @Nullable
    private Object defaultTargetDataSource;
	...
    @Nullable
    private Map<Object, DataSource> resolvedDataSources;
    @Nullable
    private DataSource resolvedDefaultDataSource;
  
  ...

最终要的三个成员变量🚩

  • targetDataSources所有数据源(需指定)
  • defaultTargetDataSource默认数据源(需指定)
  • resolvedDataSources=targetDataSources(所有数据源最终会赋值到resolvedDataSources)

多数据源执行原理🚩

思路:自定义DynameicDataSource,继承AbstractRoutingDataSource,初始化所有数据源,通过模板方法返回当前数据源标识

  1. SpringBoot启动时初始化DynameicDataSource(自定义数据源)的bean对象,此时会调用我们重写的afterPropertiesSet()方法加载所有数据源,为AbstractRoutingDataSource中的targetDataSources 初始化所有数据源,为defaultTargetDataSource 设置默认的数据源。

    public class DynameicDataSource extends AbstractRoutingDataSource{
      
      	public static ThreadLocal<String> name = new ThreadLocal<>();
      	  
      	@Autowired
      	DataSource dataSource1;
      	  
      	@Autowired
      	DataSource dataSource2;
        
        //返回当前数据源标识
        @Override
        protected Object determineCurrentLookupKey() {
            return name.get();
        }
    
        @Override
        public void afterPropertiesSet(){
    
            //1.为targetDataSources 初始化所有数据源
            HashMap<Object, Object> targetDataSources = new HashMap<>();
            targetDataSources.put("beijing",dataSource1);
            targetDataSources.put("shanghai",dataSource1);
            super.setTargetDataSources(targetDataSources);
    
            //2.为defaultTargetDataSource 设置默认的数据源
            super.setDefaultTargetDataSource(dataSource1);
          
            super.afterPropertiesSet();
        }
    
    }
    
  2. 在执行数据库操作时将当前数据源标识设置成对应DB,经过ORM框架一系列Api调用最后会执行到Spring-jdbc的getConnection()方法,根据标识拿到我们提前创建的DataSource(AbstractRoutingDataSource中的getConnection()方法),ORM框架拿到return的数据源后就开始执行对应的数据库操作。

    //AbstractRoutingDataSource中部分源码
     public Connection getConnection() throws SQLException {
         return this.determineTargetDataSource().getConnection();
     }
    
    ...
    
    protected DataSource determineTargetDataSource() {
      Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
      //获取数据源标识方法,需要子类重写
      Object lookupKey = this.determineCurrentLookupKey();
      DataSource 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 + "]");
      } else {      
        return dataSource;
      }
    }
    
  3. 通过设置自定义数据源中的标识,手动切换数据源

    @RestController
    @RequestMapping("frend")
    public class userController{
        
        @Autowired
      	private UserService userService;
      
      	public List<User> select(){
        	DynameicDataSource.name.set("beijing");
          	return userService.list();
      	}
      
        public List<User> insert(){
        	DynameicDataSource.name.set("shanghai");
          	userService.save(new User("lihw"));
      	}
    }
    

补充:

  • @Primary:如果存在多个同类型的bean,使用次注解会优先注入
  • implements InitializingBean接口,重写afterPropertiesSet();在Spring容器启动的时候会执行此方法

方案一:使用myBatis插件实现多数据源

适用场景:读写分离

执行原理

  1. 执行数据库操作时调用MyBatis对应的Api,通过Executor去执行对应的数据库操作
  2. 在执行数据库操作的方法之前会先执行插件的拦截方法,在拦截方法中设置数据源对应的标识
  3. 框架调用DataSource.getConnection()时,会来到AbstractRoutingDataSource的getConnection()方法,根据标识获取不同的数据源
  4. DynameicDataSource中重写过determineCurrentLookupKey()方法,获取当前数据源标识,从resolvedDataSources的Map中根据标识获取对应数据源

Interceptor接口源码:

public interface Interceptor{
  //拦截方法
  Object intercept(Invocation invocation) throws Throwable;
  //返回拦截器的代理对象
  Object plugin(Object object);
  //设置一些属性
  void setProperties(Properties properties);
}

代码实现

插件代理update、query方法,从拦截方法intercept()中获取执行sql的类型,实现数据源的切换。

@Intercepts(
  {@Signature(type=Executor.class,method="update",args={MappedStatement.class}),
   @Signature(type=Executor.class,method="query",args={MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class)}})
public class DynamicDataSourcePlugin implement Interceptor{
  
  @Override
  public Object intercept(Invocation invocation) throws Throwable{
    //拿到当前方法(update、query)所有参数
    Object[] objects = invocation.getArgs();
    
    //MappedStatement 封装CRUD所有的元素和SQL
    MappedStatement ms = (MappedStatement) objects[0];
    
    //修改当前数据源的key
    if(ms.getSqlCommandType().equals(SqlCommandType.SELECT)){
        DynamicDataSource.name("R");
    }else{
        DynamicDataSource.name("W");
    }
    return invocation.proceed();
  }
}

注意:需要将自定义的插件注入到IOC中,mybatis初始化的时候就会加载

public class xxConfig{
  
  @Bean
  public Interceptor dynamicDataSourcePlugin(){
    return new DynamicDataSourcePlugin();
  }
}

方案二:使用AOP+自定义注解的方式实现多数据源

1. pom依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2. 自定义注解

@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)//保留方式,可以被JVM加载
public @interface DS{
   String value() default "beijing";
}

3. 自定义多个数据源(DynameicDataSource)

初始化beijing、shanghai两个数据源

public class DynameicDataSource extends AbstractRoutingDataSource{
  
  	public static ThreadLocal<String> name = new ThreadLocal<>();
  	  
  	@Autowired
  	DataSource dataSource1;
  	  
  	@Autowired
  	DataSource dataSource2;
    
    //返回当前数据源标识
    @Override
    protected Object determineCurrentLookupKey() {
        return name.get();
    }

    @Override
    public void afterPropertiesSet(){

        //1.为targetDataSources 初始化所有数据源
        HashMap<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("beijing",beijingDataSource);
        targetDataSources.put("shanghai",shanghaiDataSource);
        super.setTargetDataSources(targetDataSources);

        //2.为defaultTargetDataSource 设置默认的数据源
        super.setDefaultTargetDataSource(dataSource1);
        super.afterPropertiesSet();
    }

}

4. 配置切面类(为ORM指定数据源)

  • @Before("within(com.lihw.dynamic.datasource.service.impl.*) && @annotation(DS)") 这个包下所有类有ds注解的都被会动态代理
  • 前置通知@Before:为ORM框架指定注解上配置的数据源
@Component
@Aspect
public class DynamicDataSourceAspect{
  
    //前置通知
    @Before("within(com.lihw.dynamic.datasource.service.impl.*) && @annotation(DS)")
  	public void before(JoinPoint joinPoint,DS ds){
        //获取注解上的数据源标识
    	String name = ds.value;
        //将标识设置到自定义的数据源上
        DynamicDataSource.name.set(name); 
  	}
}

5. 多数据源配置类

package com.lihw.dynameicdatasource.config;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import javax.sql.DataSource;

public class DataSourceConfig {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.beijingDS")
    public DataSource dataSource1(){
        //底层会自动拿到datasource1中的配置,创建一个DruidDataSource
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.shanghaiDS")
    public DataSource dataSource2(){
        //底层会自动拿到datasource2中的配置,创建一个DruidDataSource
        return DruidDataSourceBuilder.create().build();
    }
}

6. 配置文件

Spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    beijingDS:
      driver-class-name: com.mysql.cj/jdbc/Driver
      url: jdbc:mysql://127.0.0.1:3306/testdb1?serverTimezone=UTC
      username: root
      password: 1234
      initial-size: 1
      min-idle: 1
      max-active: 20
      test-on-nborrow: true
    shanghaiDS:
      driver-class-name: com.mysql.cj/jdbc/Driver
      url: jdbc:mysql://127.0.0.1:3306/testdb1?serverTimezone=UTC
      username: root
      password: 1234
      initial-size: 1
      min-idle: 1
      max-active: 20
      test-on-nborrow: true

6. controller/service

  • userController

    @RestController
    @RequestMapping("frend")
    public class userController{
        
        @Autowired
      	private UserService userService;
      
      	public List<User> select(){
          	return userService.list();
      	}
      
        public List<User> insert(){
          	userService.save(new User("lihw"));
      	}
    }
    
  • userService

    @Service
    public class UserServiceImpl implement UserService{
        
        @Autowired
      	private UserMapper userMapper;
      
      	@Autowired
        @DS("beijing")
      	public List<User> select(){
          	return userService.list();
      	}
      
      	@Autowired
        @DS("shanghai")
        public List<User> insert(){
          	userService.save(new User("lihw"));
      	}
    }
    

方案三:dynamic-datasource【开源组件实现多数据源】🚩

dynamic-datasource: 基于 SpringBoot 多数据源 动态数据源 主从分离 快速启动器 支持分布式事务 (gitee.com)

1. pom依赖

spring-boot 1.5.x 2.x.x

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
    <version>${version}</version>
</dependency>

spring-boot 1.5.x 2.x.x

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
    <version>${version}</version>
</dependency>

2. 配置数据源

  • primary: master #设置默认的数据源或者数据源组,默认值即为master
  • strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
  • slave_1:使用_可以看成slave数组,使用的时候
spring:
  datasource:
    dynamic:
      primary: master #设置默认的数据源或者数据源组,默认值即为master
      strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
      datasource:
        master:
          url: jdbc:mysql://xx.xx.xx.xx:3306/dynamic
          username: root
          password: 123456
          driver-class-name: com.mysql.jdbc.Driver # 3.2.0开始支持SPI可省略此配置
        slave_1:
          url: jdbc:mysql://xx.xx.xx.xx:3307/dynamic
          username: root
          password: 123456
          driver-class-name: com.mysql.jdbc.Driver
        slave_2:
          url: ENC(xxxxx) # 内置加密,使用请查看详细文档
          username: ENC(xxxxx)
          password: ENC(xxxxx)
          driver-class-name: com.mysql.jdbc.Driver
        #......省略
        #以上会配置一个默认库master,一个组slave下有两个子库slave_1,slave_2

3. 使用 @DS 切换数据源

@Service
@DS("slave")
public class UserServiceImpl implements UserService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public List selectAll() {
        return jdbcTemplate.queryForList("select * from user");
    }

    @Override
    @DS("slave_1")
    public List selectByCondition() {
        return jdbcTemplate.queryForList("select * from user where age >10");
    }
}
posted @ 2023-09-08 00:12  lihewei  阅读(5781)  评论(0编辑  收藏  举报
-->