MyBatis动态切换数据源,多数据源配置

依赖如下

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.5.RELEASE</version>
    </parent>
  <dependencies>
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-web</artifactId>
      </dependency>
      <dependency>
          <groupId>org.mybatis.spring.boot</groupId>
          <artifactId>mybatis-spring-boot-starter</artifactId>
          <version>1.3.2</version>
      </dependency>
      <dependency>
          <groupId>org.projectlombok</groupId>
          <artifactId>lombok</artifactId>
      </dependency>
      <dependency>
          <groupId>mysql</groupId>
          <artifactId>mysql-connector-java</artifactId>
      </dependency>
      <dependency>
          <groupId>io.springfox</groupId>
          <artifactId>springfox-swagger2</artifactId>
          <version>2.6.1</version>
      </dependency>
      <dependency>
          <groupId>io.springfox</groupId>
          <artifactId>springfox-swagger-ui</artifactId>
          <version>2.6.1</version>
      </dependency>
      <dependency>
          <groupId>com.alibaba</groupId>
          <artifactId>druid-spring-boot-starter</artifactId>
          <version>1.1.10</version>
      </dependency>
  </dependencies>
   <build>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <includes>
                    <include>**/*</include>
                </includes>
            </resource>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
            </resource>
            <resource>
                <directory>lib</directory>
                <targetPath>BOOT-INF/lib/</targetPath>
                <includes>
                    <include>**/*.jar</include>
                </includes>
            </resource>
        </resources>
    </build>

mybatis cdc 数据库变更监控 mybatis设置数据源_mybatis cdc 数据库变更监控


因此处xml位于java文件夹下,故需要上述build标签把它作为资源文件编译打包

swagger配置类

@Configuration
@EnableSwagger2
public class SwaggerConfig {

@Bean
public Docket petApi() {
    return new Docket(DocumentationType.SWAGGER_2)
            .apiInfo(apiInfo())
            .select()
            .apis(RequestHandlerSelectors.basePackage("com.study.controller")) //指定提供接口所在的基包
            .build();
}

/**
 * 该套 API 说明,包含作者、简介、版本、host、服务URL
 * @return
 */
private ApiInfo apiInfo() {
    return new ApiInfoBuilder()
            .title("demo api 说明")
            .contact(new Contact("allen","null","name@example.com"))
            .version("0.1")
            .termsOfServiceUrl("localhost:8080/demo1/")
            .description("demo api")
            .build();
}

}

配置文件

server.port=80
spring.aop.proxy-target-class=true
server.tomcat.uri-encoding=utf-8
spring.datasource.qy.name=qy
spring.datasource.qy.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.qy.url=jdbc\:mysql\://ip\:port/scm11?useUnicode\=true&characterEncoding\=utf-8&zeroDateTimeBehavior\=round&transformedBitIsBoolean\=true&useSSL\=false&allowMultiQueries\=true
spring.datasource.qy.username=root
spring.datasource.qy.password=password


spring.datasource.fqy.name=fqy
spring.datasource.fqy.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.fqy.url=jdbc\:mysql\://ip\:port/scm12?useUnicode\=true&characterEncoding\=utf-8&zeroDateTimeBehavior\=round&transformedBitIsBoolean\=true&useSSL\=false&allowMultiQueries\=true
spring.datasource.fqy.username=root
spring.datasource.fqy.password=password
#显示sql语句
spring.jpa.show-sql=true

创建动态数据源原理

mybatis cdc 数据库变更监控 mybatis设置数据源_mybatis cdc 数据库变更监控_02


由上图spring源码得知resolvedDataSources调用get一个锁返回数据源对象,若未获取到数据源则返回默认数据源resolvedDefaultDataSource,若连默认数据源都没有则报错。

public class DynamicDataSource extends AbstractRoutingDataSource {
    //继承路由数据源并复写路由规则(返回key,resolvedDataSources根据此key调用get方法获取数据源)
    @Override
    protected Object determineCurrentLookupKey() {
        //根据返回的key,从动态数据源map中get(key)取数据源
        return DatabaseContextHolder.getDatabaseType();
    }
}

用ThreadLocal绑定数据源

当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用ThreadLocal

public class DatabaseContextHolder {
    //内置一个ThreadLocal,里面有一个ThreadMap,key为线程id,value为泛型
    public static final ThreadLocal<DataBaseType> THREAD_LOCAL=new ThreadLocal();

    public DatabaseContextHolder() {
    }
    //提供get,set方法操作ThreadLocal
    public static void setDatabaseType(DataBaseType dataBaseType){
         THREAD_LOCAL.set(dataBaseType);
    }
    public static DataBaseType getDatabaseType(){
        return THREAD_LOCAL.get();
    }
    //清空ThreadLocal
    public static void removeDatabaseType(){
        THREAD_LOCAL.remove();
    }
}

自定义数据源配置类取代默认的数据源配置

@Configuration
@MapperScan(value = "com.study.dao.**", sqlSessionFactoryRef = "sqlSessionFactory")
public class DataSourcesConfig {
    @Bean(name = "qy")
    @ConfigurationProperties(prefix = "spring.datasource.qy")//取前缀为spring.datasource.qy对应的属性值填充到DruidDataSource
    public DataSource qy(){
        return new DruidDataSource();
    }
    @Bean(name = "fqy")
    @ConfigurationProperties(prefix = "spring.datasource.fqy")
    public DataSource fqy(){
        return new DruidDataSource();
    }
    @Bean
    @Primary
    public DynamicDataSource dataSource(@Qualifier("qy")DataSource qy,
                                        @Qualifier("fqy")DataSource fqy){
        Map targetSource=new HashMap<>(16);
        targetSource.put(DataBaseType.QY,qy);
        targetSource.put(DataBaseType.FQY,fqy);
        DynamicDataSource dynamicDataSource=new DynamicDataSource();
        //动态数据源需要把所有数据源map传入以供路由
        dynamicDataSource.setTargetDataSources(targetSource);
        //动态数据源需要设置一个默认数据源
        dynamicDataSource.setDefaultTargetDataSource(qy);
        //实际取哪个数据源会调用dynamicDataSource的determineCurrentLookupKey取key,再从targetSource取数据源。
        return dynamicDataSource;
    }
    //sqlsession也要指定使用的数据源
    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception{
        SqlSessionFactoryBean sqlSessionFactoryBean=new SqlSessionFactoryBean();
        //设置工厂的每个bean使用的数据源和适用范围
        sqlSessionFactoryBean.setDataSource(dataSource(qy(),fqy()));
        sqlSessionFactoryBean.setMapperLocations(
               new  PathMatchingResourcePatternResolver().getResources("classpath:com/study/dao/**/*.xml")
        );
        return sqlSessionFactoryBean.getObject();
    }
    //事务也要指定使用的数据源
    @Bean
    public DataSourceTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dataSource(qy(), fqy()));
    }
}

背景:企业qy用户登录时qy放到Head里,qy企业和非企业fqy是两个数据。需求:qy用户调用插入notice表中也要往fqy数据库中插入此条数据,fqy用户调用插入notice表中也要往qy数据库中插入此条数据

mybatis cdc 数据库变更监控 mybatis设置数据源_mybatis_03


CREATE TABLE notice (

id bigint(20) NOT NULL AUTO_INCREMENT,

title varchar(255) DEFAULT NULL,

status int(11) DEFAULT NULL,

PRIMARY KEY (id)

)

调用接口时根据传入的dataType决定启用的数据库

(1)过滤器(Filter):它依赖于servlet容器。在实现上,基于函数回调,它可以对几乎所有请求进行过滤,但是缺点是一个过滤器实例只能在容器初始化时调用一次。使用过滤器的目的,是用来做一些过滤操作,获取我们想要获取的数据,比如:在Javaweb中,对传入的request、response提前过滤掉一些信息,或者提前设置一些参数,然后再传入servlet或者Controller进行业务逻辑操作。通常用的场景是:在过滤器中修改字符编码(CharacterEncodingFilter)、在过滤器中修改HttpServletRequest的一些参数(XSSFilter(自定义过滤器)),如:过滤低俗文字、危险字符等。
(2)拦截器(Interceptor):它依赖于web框架,在SpringMVC中就是依赖于SpringMVC框架。在实现上,基于Java的反射机制,属于面向切面编程(AOP)的一种运用,就是在service或者一个方法前,调用一个方法,或者在方法后,调用一个方法,比如动态代理就是拦截器的简单实现,在调用方法前打印出字符串(或者做其它业务逻辑的操作),也可以在调用方法后打印出字符串,甚至在抛出异常的时候做业务逻辑的操作。由于拦截器是基于web框架的调用,因此可以使用Spring的依赖注入(DI)进行一些业务操作,同时一个拦截器实例在一个controller生命周期之内可以多次调用。但是缺点是只能对controller请求进行拦截,对其他的一些比如直接访问静态资源的请求则没办法进行拦截处理。

IOC与DI区别:依赖注入是从应用程序的角度在描述:应用程序依赖容器创建并注入它所需要的外部资源;而控制反转是从容器的角度在描述:容器控制应用程序,由容器反向的向应用程序注入应用程序所需要的外部资源。
故此处选择拦截器

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    //实现添加拦截器
    public void addInterceptors(InterceptorRegistry registry){
        registry.addInterceptor(new HandlerInterceptorAdapter(){
            //拦截逻辑
            public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
                String dataType = request.getHeader("dataType");
                // 此为全局拦截器, 打开swagger时 dataType 为空, 调用接口时 dataType为 必传字段(非空)
                if(!StringUtils.isEmpty(dataType)){
                    if (DataBaseType.QY.toString().equalsIgnoreCase(dataType)) {
                        DatabaseContextHolder.setDatabaseType(DataBaseType.QY);
                    } else if (DataBaseType.FQY.toString().equalsIgnoreCase(dataType)) {
                        DatabaseContextHolder.setDatabaseType(DataBaseType.FQY);
                    }else {
                        //两者都不属于时不允许访问
                        response.getWriter().close();
                        return false;
                    }
                }
                return true;//不传时放行(默认数据源)
            }

        }).addPathPatterns("/**");
    }
}

代码(简略版,就不那么规范了)

@RestController
@RequestMapping("/notice")
public class NoticeController {
    @Autowired
    private NoticeService noticeService;
    @RequestMapping(value = "/add",method = RequestMethod.POST)
    public String add(@@RequestHeader("dataType")String dataType,@RequestBody Notice notice){
        try {
            noticeService.add(notice);
        } catch (Exception e) {
            e.printStackTrace();
            return "失败";
        }
        return "成功";
    }
}

切换数据源的代码切不可放在带有事务的方法上,否则无法切换

@Service
public class NoticeService {
    @Autowired
    private NoticeMapper noticeMapper;
    /**
     *切换
     */
 public void addMethod(Notice notice){
        add1(notice);
        DataBaseType databaseType = DatabaseContextHolder.getDatabaseType();
        if( databaseType.toString().equalsIgnoreCase("qy")){
            DatabaseContextHolder.setDatabaseType(DataBaseType.FQY);
        }else {
            DatabaseContextHolder.setDatabaseType(DataBaseType.QY);
        }
        add2(notice);
        DatabaseContextHolder.removeDatabaseType();
        add1(notice);
    }
    @Transactional
    public void add1(Notice notice) {
        noticeMapper.add(notice);
    }
    @Transactional
    public void add2(Notice notice) {
        noticeMapper.add(notice);
    }
}
public interface NoticeMapper  {
    void add(Notice notice);
}
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.study.dao.NoticeMapper">
    
    <insert id="add" keyProperty="id" useGeneratedKeys="true">
        insert into notice(title,status) values (#{title},#{status})
    </insert>
</mapper>
@Data
public class Notice {

    private int id;

    private String title;

    /**
     * 状态:1保存;2发布
     */
    private int status;
}

已知缺陷

public void addMethod(Notice notice){
       add1(notice);
        DataBaseType databaseType = DatabaseContextHolder.getDatabaseType();
       if( databaseType.toString().equalsIgnoreCase("qy")){
           DatabaseContextHolder.setDatabaseType(DataBaseType.FQY);
       }else {
           DatabaseContextHolder.setDatabaseType(DataBaseType.QY);
       }
        System.out.println(1/0);
       add2(notice);
       DatabaseContextHolder.removeDatabaseType();
       add1(notice);
    }