天天看點

MyBatis動态切換資料源/多資料源配置配置檔案用ThreadLocal綁定資料源

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動态切換資料源/多資料源配置配置檔案用ThreadLocal綁定資料源

是以處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","[email protected]"))
            .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動态切換資料源/多資料源配置配置檔案用ThreadLocal綁定資料源

由上圖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動态切換資料源/多資料源配置配置檔案用ThreadLocal綁定資料源

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);
    }
           

此時add1插庫成功,add2插庫不成功,要想兩個庫一起成功/失敗,有何良策?