一区二区三区在线-一区二区三区亚洲视频-一区二区三区亚洲-一区二区三区午夜-一区二区三区四区在线视频-一区二区三区四区在线免费观看

服務器之家:專注于服務器技術及軟件下載分享
分類導航

PHP教程|ASP.NET教程|JAVA教程|ASP教程|編程技術|正則表達式|

服務器之家 - 編程語言 - JAVA教程 - Springcloud+Mybatis使用多數(shù)據(jù)源的四種方式(小結)

Springcloud+Mybatis使用多數(shù)據(jù)源的四種方式(小結)

2020-09-24 00:31java架構師小芷 JAVA教程

這篇文章主要介紹了Springcloud+Mybatis使用多數(shù)據(jù)源的四種方式,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧

前段時間在做會員中心和中間件系統(tǒng)開發(fā)時,多次碰到多數(shù)據(jù)源配置問題,主要用到分包方式、參數(shù)化切換、注解+AOP、動態(tài)添加 這四種方式。這里做一下總結,分享下使用心得以及踩過的坑。

分包方式

 

數(shù)據(jù)源配置文件

在yml中,配置兩個數(shù)據(jù)源,id分別為master和s1。

spring:
 datasource:
  master:
   jdbcUrl: jdbc:mysql://192.168.xxx.xxx:xxxx/db1?.........
   username: xxx
   password: xxx
   driverClassName: com.mysql.cj.jdbc.Driver
  s1:
   jdbcUrl: jdbc:mysql://192.168.xxx.xxx:xxxx/db2?........
   username: xxx
   password: xxx
   driverClassName: com.mysql.cj.jdbc.Driver

數(shù)據(jù)源配置類

 master數(shù)據(jù)源配置類

注意點:

需要用@Primary注解指定默認數(shù)據(jù)源,否則spring不知道哪個是主數(shù)據(jù)源;

@Configuration
@MapperScan(basePackages = "com.hosjoy.xxx.xxx.xxx.xxx.mapper.master", sqlSessionFactoryRef = "masterSqlSessionFactory")
public class MasterDataSourceConfig {

  //默認數(shù)據(jù)源
  @Bean(name = "masterDataSource")
  @Primary
  @ConfigurationProperties(prefix = "spring.datasource.master")
  public HikariDataSource masterDataSource() {
    return DataSourceBuilder.create().type(HikariDataSource.class).build();
  }

  @Bean(name = "masterSqlSessionFactory")
  @Primary
  public SqlSessionFactory masterSqlSessionFactory(@Qualifier("masterDataSource") DataSource datasource, PaginationInterceptor paginationInterceptor)
      throws Exception {
    MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
    bean.setDataSource(datasource);
    bean.setMapperLocations(
        // 設置mybatis的xml所在位置
        new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/master/**/**.xml"));
    bean.setPlugins(new Interceptor[]{paginationInterceptor});
    return bean.getObject();
  }

  @Bean(name = "masterSqlSessionTemplate")
  @Primary
  public SqlSessionTemplate masterSqlSessionTemplate(
      @Qualifier("masterSqlSessionFactory") SqlSessionFactory sessionfactory) {
    return new SqlSessionTemplate(sessionfactory);
  }
}

s1數(shù)據(jù)源配置類

@Configuration
@MapperScan(basePackages = "com.hosjoy.xxx.xxx.xxx.xxx.mapper.s1", sqlSessionFactoryRef = "s1SqlSessionFactory")
public class S1DataSourceConfig {

  @Bean(name = "s1DataSource")
  @ConfigurationProperties(prefix = "spring.datasource.s1")
  public HikariDataSource s1DataSource() {
    return DataSourceBuilder.create().type(HikariDataSource.class).build();
  }

  @Bean(name = "s1SqlSessionFactory")
  public SqlSessionFactory s1SqlSessionFactory(@Qualifier("s1DataSource") DataSource datasource
      , PaginationInterceptor paginationInterceptor)
      throws Exception {
    MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
    bean.setDataSource(datasource);
    bean.setMapperLocations(
        // 設置mybatis的xml所在位置
        new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/s1/**/**.xml"));
    bean.setPlugins(new Interceptor[]{paginationInterceptor});
    return bean.getObject();
  }

  @Bean(name = "s1SqlSessionTemplate")
  public SqlSessionTemplate s1SqlSessionTemplate(
      @Qualifier("s1SqlSessionFactory") SqlSessionFactory sessionfactory) {
    return new SqlSessionTemplate(sessionfactory);
  }
}

使用

可以看出,mapper接口、xml文件,需要按照不同的數(shù)據(jù)源分包。在操作數(shù)據(jù)庫時,根據(jù)需要在service類中注入dao層。

特點分析

優(yōu)點

實現(xiàn)起來簡單,只需要編寫數(shù)據(jù)源配置文件和配置類,mapper接口和xml文件注意分包即可。

缺點

很明顯,如果后面要增加或刪除數(shù)據(jù)源,不僅要修改數(shù)據(jù)源配置文件,還需要修改配置類。

例如增加一個數(shù)據(jù)源,同時還需要新寫一個該數(shù)據(jù)源的配置類,同時還要考慮新建mapper接口包、xml包等,沒有實現(xiàn) “熱插拔” 效果。

參數(shù)化切換方式

 

 思想

參數(shù)化切換數(shù)據(jù)源,意思是,業(yè)務側需要根據(jù)當前業(yè)務參數(shù),動態(tài)的切換到不同的數(shù)據(jù)源。

這與分包思想不同。分包的前提是在編寫代碼的時候,就已經知道當前需要用哪個數(shù)據(jù)源,而參數(shù)化切換數(shù)據(jù)源需要根據(jù)業(yè)務參數(shù)決定用哪個數(shù)據(jù)源。

例如,請求參數(shù)userType值為1時,需要切換到數(shù)據(jù)源slave1;請求參數(shù)userType值為2時,需要切換到數(shù)據(jù)源slave2。

/**偽代碼**/
int userType = reqUser.getType();
if (userType == 1){
  //切換到數(shù)據(jù)源slave1
  //數(shù)據(jù)庫操作
} else if(userType == 2){
  //切換到數(shù)據(jù)源slave2
  //數(shù)據(jù)庫操作
}

設計思路

 數(shù)據(jù)源注冊

數(shù)據(jù)源配置類創(chuàng)建datasource時,從yml配置文件中讀取所有數(shù)據(jù)源配置,自動創(chuàng)建每個數(shù)據(jù)源,并注冊至bean工廠和AbstractRoutingDatasource(后面聊聊這個),同時返回默認的數(shù)據(jù)源master。

Springcloud+Mybatis使用多數(shù)據(jù)源的四種方式(小結)

 數(shù)據(jù)源切換

(1)通過線程池處理請求,每個請求獨占一個線程,這樣每個線程切換數(shù)據(jù)源時互不影響。

(2)根據(jù)業(yè)務參數(shù)獲取應切換的數(shù)據(jù)源ID,根據(jù)ID從數(shù)據(jù)源緩存池獲取數(shù)據(jù)源bean;

(3)生成當前線程數(shù)據(jù)源key;

(4)將key設置到threadLocal;

(5)將key和數(shù)據(jù)源bean放入數(shù)據(jù)源緩存池;

(6)在執(zhí)行mapper方法前,spring會調用determineCurrentLookupKey方法獲取key,然后根據(jù)key去數(shù)據(jù)源緩存池取出數(shù)據(jù)源,然后getConnection獲取該數(shù)據(jù)源連接;

(7)使用該數(shù)據(jù)源執(zhí)行數(shù)據(jù)庫操作;

(8)釋放當前線程數(shù)據(jù)源。

Springcloud+Mybatis使用多數(shù)據(jù)源的四種方式(小結)

AbstractRoutingDataSource源碼分析

spring為我們提供了AbstractRoutingDataSource抽象類,該類就是實現(xiàn)動態(tài)切換數(shù)據(jù)源的關鍵。

我們看下該類的類圖,其實現(xiàn)了DataSource接口。

Springcloud+Mybatis使用多數(shù)據(jù)源的四種方式(小結)

我們看下它的getConnection方法的邏輯,其首先調用determineTargetDataSource來獲取數(shù)據(jù)源,再獲取數(shù)據(jù)庫連接。很容易猜想到就是這里來決定具體使用哪個數(shù)據(jù)源的。

Springcloud+Mybatis使用多數(shù)據(jù)源的四種方式(小結)

進入到determineTargetDataSource方法,我們可以看到它先是調用determineCurrentLookupKey獲取到一個lookupKey,然后根據(jù)這個key去resolvedDataSources里去找相應的數(shù)據(jù)源。

Springcloud+Mybatis使用多數(shù)據(jù)源的四種方式(小結)

看下該類定義的幾個對象,defaultTargetDataSource是默認數(shù)據(jù)源,resolvedDataSources是一個map對象,存儲所有主從數(shù)據(jù)源。

Springcloud+Mybatis使用多數(shù)據(jù)源的四種方式(小結)

所以,關鍵就是這個lookupKey的獲取邏輯,決定了當前獲取的是哪個數(shù)據(jù)源,然后執(zhí)行getConnection等一系列操作。determineCurrentLookupKey是AbstractRoutingDataSource類中的一個抽象方法,而它的返回值是你所要用的數(shù)據(jù)源dataSource的key值,有了這個key值,resolvedDataSource(這是個map,由配置文件中設置好后存入的)就從中取出對應的DataSource,如果找不到,就用配置默認的數(shù)據(jù)源。

所以,通過擴展AbstractRoutingDataSource類,并重寫其中的determineCurrentLookupKey()方法,可以實現(xiàn)數(shù)據(jù)源的切換。

代碼實現(xiàn)

下面貼出關鍵代碼實現(xiàn)。

數(shù)據(jù)源配置文件

這里配了3個數(shù)據(jù)源,其中主數(shù)據(jù)源是MySQL,兩個從數(shù)據(jù)源是sqlserver。

spring:
 datasource:
  master:
   jdbcUrl: jdbc:mysql://192.168.xx.xxx:xxx/db1?........
   username: xxx
   password: xxx
   driverClassName: com.mysql.cj.jdbc.Driver
  slave1:
   jdbcUrl: jdbc:sqlserver://192.168.xx.xxx:xxx;DatabaseName=db2
   username: xxx
   password: xxx
   driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver
  slave2:
   jdbcUrl: jdbc:sqlserver://192.168.xx.xxx:xxx;DatabaseName=db3
   username: xxx
   password: xxx
   driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver

定義動態(tài)數(shù)據(jù)源

主要是繼承AbstractRoutingDataSource,實現(xiàn)determineCurrentLookupKey方法。

public class DynamicDataSource extends AbstractRoutingDataSource {
  /*存儲所有數(shù)據(jù)源*/
  private Map<Object, Object> backupTargetDataSources;

  public Map<Object, Object> getBackupTargetDataSources() {
    return backupTargetDataSources;
  }
  /*defaultDataSource為默認數(shù)據(jù)源*/
  public DynamicDataSource(DataSource defaultDataSource, Map<Object, Object> targetDataSource) {
    backupTargetDataSources = targetDataSource;
    super.setDefaultTargetDataSource(defaultDataSource);
    super.setTargetDataSources(backupTargetDataSources);
    super.afterPropertiesSet();
  }
  public void addDataSource(String key, DataSource dataSource) {
    this.backupTargetDataSources.put(key, dataSource);
    super.setTargetDataSources(this.backupTargetDataSources);
    super.afterPropertiesSet();
  }
  /*返回當前線程的數(shù)據(jù)源的key*/
  @Override
  protected Object determineCurrentLookupKey() {
    return DynamicDataSourceContextHolder.getContextKey();
  }
}

定義數(shù)據(jù)源key線程變量持有

定義一個ThreadLocal靜態(tài)變量,該變量持有了線程和線程的數(shù)據(jù)源key之間的關系。當我們要切換數(shù)據(jù)源時,首先要自己生成一個key,將這個key存入threadLocal線程變量中;同時還應該從DynamicDataSource對象中的backupTargetDataSources屬性中獲取到數(shù)據(jù)源對象, 然后將key和數(shù)據(jù)源對象再put到backupTargetDataSources中。 這樣,spring就能根據(jù)determineCurrentLookupKey方法返回的key,從backupTargetDataSources中取出我們剛剛設置的數(shù)據(jù)源對象,進行getConnection等一系列操作了。

public class DynamicDataSourceContextHolder {
  /**
   * 存儲線程和數(shù)據(jù)源key的映射關系
   */
  private static final ThreadLocal<String> DATASOURCE_CONTEXT_KEY_HOLDER = new ThreadLocal<>();

  /***
   * 設置當前線程數(shù)據(jù)源key
   */
  public static void setContextKey(String key) {
    DATASOURCE_CONTEXT_KEY_HOLDER.set(key);
  }
  /***
   * 獲取當前線程數(shù)據(jù)源key
   */
  public static String getContextKey() {
    String key = DATASOURCE_CONTEXT_KEY_HOLDER.get();
    return key == null ? DataSourceConstants.DS_KEY_MASTER : key;
  }
  /***
   * 刪除當前線程數(shù)據(jù)源key
   */
  public static void removeContextKey() {
    DynamicDataSource dynamicDataSource = RequestHandleMethodRegistry.getContext().getBean(DynamicDataSource.class);
    String currentKey = DATASOURCE_CONTEXT_KEY_HOLDER.get();
    if (StringUtils.isNotBlank(currentKey) && !"master".equals(currentKey)) {
      dynamicDataSource.getBackupTargetDataSources().remove(currentKey);
    }
    DATASOURCE_CONTEXT_KEY_HOLDER.remove();
  }
}

多數(shù)據(jù)源自動配置類

這里通過讀取yml配置文件中所有數(shù)據(jù)源的配置,自動為每個數(shù)據(jù)源創(chuàng)建datasource 對象并注冊至bean工廠。同時將這些數(shù)據(jù)源對象,設置到AbstractRoutingDataSource中。

通過這種方式,后面如果需要添加或修改數(shù)據(jù)源,都無需新增或修改java配置類,只需去配置中心修改yml文件即可。

@Configuration
@MapperScan(basePackages = "com.hosjoy.xxx.xxx.modules.xxx.mapper")
public class DynamicDataSourceConfig {
  @Autowired
  private BeanFactory beanFactory;
  @Autowired
  private DynamicDataSourceProperty dynamicDataSourceProperty;
  /**
   * 功能描述: <br>
   * 〈動態(tài)數(shù)據(jù)源bean 自動配置注冊所有數(shù)據(jù)源〉
   *
   * @param
   * @return javax.sql.DataSource
   * @Author li.he
   * @Date 2020/6/4 16:47
   * @Modifier
   */
  @Bean
  @Primary
  public DataSource dynamicDataSource() {
    DefaultListableBeanFactory listableBeanFactory = (DefaultListableBeanFactory) beanFactory;
    /*獲取yml所有數(shù)據(jù)源配置*/
    Map<String, Object> datasource = dynamicDataSourceProperty.getDatasource();
    Map<Object, Object> dataSourceMap = new HashMap<>(5);
    Optional.ofNullable(datasource).ifPresent(map -> {
      for (Map.Entry<String, Object> entry : map.entrySet()) {
        //創(chuàng)建數(shù)據(jù)源對象
        HikariDataSource dataSource = (HikariDataSource) DataSourceBuilder.create().build();
        String dataSourceId = entry.getKey();
        configeDataSource(entry, dataSource);
        /*bean工廠注冊每個數(shù)據(jù)源bean*/
        listableBeanFactory.registerSingleton(dataSourceId, dataSource);
        dataSourceMap.put(dataSourceId, dataSource);
      }
    });
    //AbstractRoutingDataSource設置主從數(shù)據(jù)源
    return new DynamicDataSource(beanFactory.getBean("master", DataSource.class),     dataSourceMap);
  }

  private void configeDataSource(Map.Entry<String, Object> entry, HikariDataSource dataSource) {
    Map<String, Object> dataSourceConfig = (Map<String, Object>) entry.getValue();
    dataSource.setJdbcUrl(MapUtils.getString(dataSourceConfig, "jdbcUrl"));
    dataSource.setDriverClassName(MapUtils.getString(dataSourceConfig, "driverClassName"));
    dataSource.setUsername(MapUtils.getString(dataSourceConfig, "username"));
    dataSource.setPassword(MapUtils.getString(dataSourceConfig, "password"));
  }

}

數(shù)據(jù)源切換工具類

切換邏輯:

(1)生成當前線程數(shù)據(jù)源key

(2)根據(jù)業(yè)務條件,獲取應切換的數(shù)據(jù)源ID;

(3)根據(jù)ID從數(shù)據(jù)源緩存池中獲取數(shù)據(jù)源對象,并再次添加到backupTargetDataSources緩存池中;

(4)threadLocal設置當前線程對應的數(shù)據(jù)源key;

(5)在執(zhí)行數(shù)據(jù)庫操作前,spring會調用determineCurrentLookupKey方法獲取key,然后根據(jù)key去數(shù)據(jù)源緩存池取出數(shù)據(jù)源,然后getConnection獲取該數(shù)據(jù)源連接;

(6)使用該數(shù)據(jù)源執(zhí)行數(shù)據(jù)庫操作;

(7)釋放緩存:threadLocal清理當前線程數(shù)據(jù)源信息、數(shù)據(jù)源緩存池清理當前線程數(shù)據(jù)源key和數(shù)據(jù)源對象,目的是防止內存泄漏。

@Slf4j
@Component
public class DataSourceUtil {
  @Autowired
  private DataSourceConfiger dataSourceConfiger;
  
  /*根據(jù)業(yè)務條件切換數(shù)據(jù)源*/
  public void switchDataSource(String key, Predicate<? super Map<String, Object>> predicate) {
    try {
      //生成當前線程數(shù)據(jù)源key
      String newDsKey = System.currentTimeMillis() + "";
      List<Map<String, Object>> configValues = dataSourceConfiger.getConfigValues(key);
      Map<String, Object> db = configValues.stream().filter(predicate)
          .findFirst().get();
      String id = MapUtils.getString(db, "id");
      //根據(jù)ID從數(shù)據(jù)源緩存池中獲取數(shù)據(jù)源對象,并再次添加到backupTargetDataSources
      addDataSource(newDsKey, id);
      //設置當前線程對應的數(shù)據(jù)源key
      DynamicDataSourceContextHolder.setContextKey(newDsKey);
      log.info("當前線程數(shù)據(jù)源切換成功,當前數(shù)據(jù)源ID:{}", id);

    }
    catch (Exception e) {
      log.error("切換數(shù)據(jù)源失敗,請檢查數(shù)據(jù)源配置文件。key:{}, 條件:{}", key, predicate.toString());
      throw new ClientException("切換數(shù)據(jù)源失敗,請檢查數(shù)據(jù)源配置", e);
    }
  }
  
  /*將數(shù)據(jù)源添加至多數(shù)據(jù)源緩存池中*/
  public static void addDataSource(String key, String dataSourceId) {
    DynamicDataSource dynamicDataSource = RequestHandleMethodRegistry.getContext().getBean(DynamicDataSource.class);
    DataSource dataSource = (DataSource) dynamicDataSource.getBackupTargetDataSources().get(dataSourceId);
    dynamicDataSource.addDataSource(key, dataSource);
  }
}

使用

public void doExecute(ReqTestParams reqTestParams){
  //構造條件
  Predicate<? super Map<String, Object>> predicate =.........;
  //切換數(shù)據(jù)源
  dataSourceUtil.switchDataSource("testKey", predicate);
  //數(shù)據(jù)庫操作
  mapper.testQuery();
  //清理緩存,避免內存泄漏
  DynamicDataSourceContextHolder.removeContextKey();
}

每次數(shù)據(jù)源使用后,都要調用removeContextKey方法清理緩存,避免內存泄漏,這里可以考慮用AOP攔截特定方法,利用后置通知為執(zhí)行方法代理執(zhí)行緩存清理工作。

@Aspect
@Component
@Slf4j
public class RequestHandleMethodAspect {
  @After("xxxxxxxxxxxxxxExecution表達式xxxxxxxxxxxxxxxxxx")
  public void afterRunning(JoinPoint joinPoint){
    String name = joinPoint.getSignature().toString();
    long id = Thread.currentThread().getId();
    log.info("方法執(zhí)行完畢,開始清空當前線程數(shù)據(jù)源,線程id:{},代理方法:{}",id,name);
    DynamicDataSourceContextHolder.removeContextKey();
    log.info("當前線程數(shù)據(jù)源清空完畢,已返回至默認數(shù)據(jù)源:{}",id);
  }
}

特點分析

(1)參數(shù)化切換數(shù)據(jù)源方式,出發(fā)點和分包方式不一樣,適合于在運行時才能確定用哪個數(shù)據(jù)源。

(2)需要手動執(zhí)行切換數(shù)據(jù)源操作;

(3)無需分包,mapper和xml路徑自由定義;

(4)增加數(shù)據(jù)源,無需修改java配置類,只需修改數(shù)據(jù)源配置文件即可。

注解方式

 

思想

該方式利用注解+AOP思想,為需要切換數(shù)據(jù)源的方法標記自定義注解,注解屬性指定數(shù)據(jù)源ID,然后利用AOP切面攔截注解標記的方法,在方法執(zhí)行前,切換至相應數(shù)據(jù)源;在方法執(zhí)行結束后,切換至默認數(shù)據(jù)源。

需要注意的是,自定義切面的優(yōu)先級需要高于@Transactional注解對應切面的優(yōu)先級。

否則,在自定義注解和@Transactional同時使用時,@Transactional切面會優(yōu)先執(zhí)行,切面在調用getConnection方法時,會去調用AbstractRoutingDataSource.determineCurrentLookupKey方法,此時獲取到的是默認數(shù)據(jù)源master。這時@UsingDataSource對應的切面即使再設置當前線程的數(shù)據(jù)源key,后面也不會再去調用determineCurrentLookupKey方法來切換數(shù)據(jù)源了。

設計思路

數(shù)據(jù)源注冊

同上。

數(shù)據(jù)源切換

利用切面,攔截所有@UsingDataSource注解標記的方法,根據(jù)dataSourceId屬性,在方法執(zhí)行前,切換至相應數(shù)據(jù)源;在方法執(zhí)行結束后,清理緩存并切換至默認數(shù)據(jù)源。

Springcloud+Mybatis使用多數(shù)據(jù)源的四種方式(小結)

代碼實現(xiàn)

數(shù)據(jù)源配置文件

同上。

定義動態(tài)數(shù)據(jù)源

同上。

定義數(shù)據(jù)源key線程變量持有

同上。

多數(shù)據(jù)源自動配置類

同上。

數(shù)據(jù)源切換工具類

切換邏輯:

(1)生成當前線程數(shù)據(jù)源key

(3)根據(jù)ID從數(shù)據(jù)源緩存池中獲取數(shù)據(jù)源對象,并再次添加到backupTargetDataSources緩存池中;

(4)threadLocal設置當前線程對應的數(shù)據(jù)源key;

(5)在執(zhí)行數(shù)據(jù)庫操作前,spring會調用determineCurrentLookupKey方法獲取key,然后根據(jù)key去數(shù)據(jù)源緩存池取出數(shù)據(jù)源,然后getConnection獲取該數(shù)據(jù)源連接;

(6)使用該數(shù)據(jù)源執(zhí)行數(shù)據(jù)庫操作;

(7)釋放緩存:threadLocal清理當前線程數(shù)據(jù)源信息、數(shù)據(jù)源緩存池清理當前線程數(shù)據(jù)源key和數(shù)據(jù)源對象。

public static void switchDataSource(String dataSourceId) {
  if (StringUtils.isBlank(dataSourceId)) {
    throw new ClientException("切換數(shù)據(jù)源失敗,數(shù)據(jù)源ID不能為空");
  }
  try {
    String threadDataSourceKey = UUID.randomUUID().toString();
    DataSourceUtil.addDataSource(threadDataSourceKey, dataSourceId);
    DynamicDataSourceContextHolder.setContextKey(threadDataSourceKey);
  }
  catch (Exception e) {
    log.error("切換數(shù)據(jù)源失敗,未找到指定的數(shù)據(jù)源,請確保所指定的數(shù)據(jù)源ID已在配置文件中配置。dataSourceId:{}", dataSourceId);
    throw new ClientException("切換數(shù)據(jù)源失敗,未找到指定的數(shù)據(jù)源,請確保所指定的數(shù)據(jù)源ID已在配置文件中配置。dataSourceId:" + dataSourceId, e);
  }
}

自定義注解

自定義注解標記當前方法所使用的數(shù)據(jù)源,默認為主數(shù)據(jù)源。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UsingDataSource {

  String dataSourceId() default "master";
}

切面

主要是定義前置通知和后置通知,攔截UsingDataSource注解標記的方法,方法執(zhí)行前切換數(shù)據(jù)源,方法執(zhí)行后清理數(shù)據(jù)源緩存。

需要標記切面的優(yōu)先級比@Transaction注解對應切面的優(yōu)先級要高。否則,在自定義注解和@Transactional同時使用時,@Transactional切面會優(yōu)先執(zhí)行,切面在調用getConnection方法時,會去調用AbstractRoutingDataSource.determineCurrentLookupKey方法,此時獲取到的是默認數(shù)據(jù)源master。這時@UsingDataSource對應的切面即使再設置當前線程的數(shù)據(jù)源key,后面也不會再去調用determineCurrentLookupKey方法來切換數(shù)據(jù)源了。

@Aspect
@Component
@Slf4j
@Order(value = 1)
public class DynamicDataSourceAspect {

  //攔截UsingDataSource注解標記的方法,方法執(zhí)行前切換數(shù)據(jù)源
  @Before(value = "@annotation(usingDataSource)")
  public void before(JoinPoint joinPoint, UsingDataSource usingDataSource) {
    String dataSourceId = usingDataSource.dataSourceId();
    log.info("執(zhí)行目標方法前開始切換數(shù)據(jù)源,目標方法:{}, dataSourceId:{}", joinPoint.getSignature().toString(), dataSourceId);
    try {
      DataSourceUtil.switchDataSource(dataSourceId);
    }
    catch (Exception e) {
      log.error("切換數(shù)據(jù)源失??!數(shù)據(jù)源可能未配置或不可用,數(shù)據(jù)源ID:{}", dataSourceId, e);
      throw new ClientException("切換數(shù)據(jù)源失??!數(shù)據(jù)源可能未配置或不可用,數(shù)據(jù)源ID:" + dataSourceId, e);
    }
    log.info("目標方法:{} , 已切換至數(shù)據(jù)源:{}", joinPoint.getSignature().toString(), dataSourceId);
  }

  //攔截UsingDataSource注解標記的方法,方法執(zhí)行后清理數(shù)據(jù)源,防止內存泄漏
  @After(value = "@annotation(com.hosjoy.hbp.dts.common.annotation.UsingDataSource)")
  public void after(JoinPoint joinPoint) {
    log.info("目標方法執(zhí)行完畢,執(zhí)行清理,切換至默認數(shù)據(jù)源,目標方法:{}", joinPoint.getSignature().toString());
    try {
      DynamicDataSourceContextHolder.removeContextKey();
    }
    catch (Exception e) {
      log.error("清理數(shù)據(jù)源失敗", e);
      throw new ClientException("清理數(shù)據(jù)源失敗", e);
    }
    log.info("目標方法:{} , 數(shù)據(jù)源清理完畢,已返回默認數(shù)據(jù)源", joinPoint.getSignature().toString());
  }
}

使用

@UsingDataSource(dataSourceId = "slave1")
@Transactional
public void test(){
  AddressPo po = new AddressPo();
  po.setMemberCode("asldgjlk");
  po.setName("lihe");
  po.setPhone("13544986666");
  po.setProvince("asdgjwlkgj");
  addressMapper.insert(po);
  int i = 1 / 0;
}

動態(tài)添加方式(非常用)

 

 業(yè)務場景描述

這種業(yè)務場景不是很常見,但肯定是有人遇到過的。

項目里面只配置了1個默認的數(shù)據(jù)源,而具體運行時需要動態(tài)的添加新的數(shù)據(jù)源,非已配置好的靜態(tài)的多數(shù)據(jù)源。例如需要去服務器實時讀取數(shù)據(jù)源配置信息(非配置在本地),然后再執(zhí)行數(shù)據(jù)庫操作。

這種業(yè)務場景,以上3種方式就都不適用了,因為上述的數(shù)據(jù)源都是提前在yml文件配制好的。

實現(xiàn)思路

除了第6步外,利用之前寫好的代碼就可以實現(xiàn)。

思路是:

(1)創(chuàng)建新數(shù)據(jù)源;

(2)DynamicDataSource注冊新數(shù)據(jù)源;

(3)切換:設置當前線程數(shù)據(jù)源key;添加臨時數(shù)據(jù)源;

(4)數(shù)據(jù)庫操作(必須在另一個service實現(xiàn),否則無法控制事務);

(5)清理當前線程數(shù)據(jù)源key、清理臨時數(shù)據(jù)源;

(6)清理剛剛注冊的數(shù)據(jù)源;

(7)此時已返回至默認數(shù)據(jù)源。

代碼

代碼寫的比較粗陋,但是模板大概就是這樣子,主要想表達實現(xiàn)的方式。

Service A:

public String testUsingNewDataSource(){
    DynamicDataSource dynamicDataSource = RequestHandleMethodRegistry.getContext().getBean("dynamicDataSource", DynamicDataSource.class);
    try {
      //模擬從服務器讀取數(shù)據(jù)源信息
      //..........................
      //....................
      
      //創(chuàng)建新數(shù)據(jù)源
      HikariDataSource dataSource = (HikariDataSource)          DataSourceBuilder.create().build();
      dataSource.setJdbcUrl("jdbc:mysql://192.168.xxx.xxx:xxxx/xxxxx?......");
      dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
      dataSource.setUsername("xxx");
      dataSource.setPassword("xxx");
      
      //DynamicDataSource注冊新數(shù)據(jù)源
      dynamicDataSource.addDataSource("test_ds_id", dataSource);

      //設置當前線程數(shù)據(jù)源key、添加臨時數(shù)據(jù)源
      DataSourceUtil.switchDataSource("test_ds_id");

      //數(shù)據(jù)庫操作(必須在另一個service實現(xiàn),否則無法控制事務)
      serviceB.testInsert();
    }
    finally {
      //清理當前線程數(shù)據(jù)源key
      DynamicDataSourceContextHolder.removeContextKey();

      //清理剛剛注冊的數(shù)據(jù)源
      dynamicDataSource.removeDataSource("test_ds_id");

    }
    return "aa";
  }

Service B:

@Transactional(rollbackFor = Exception.class)
  public void testInsert() {
    AddressPo po = new AddressPo();
    po.setMemberCode("555555555");
    po.setName("李郃");
    po.setPhone("16651694996");
    po.setProvince("江蘇省");
    po.setCity("南京市");
    po.setArea("浦口區(qū)");
    po.setAddress("南京市浦口區(qū)寧六路219號");
    po.setDef(false);
    po.setCreateBy("23958");
    addressMapper.insert(po);
    //測試事務回滾
    int i = 1 / 0;
  }

DynamicDataSource: 增加removeDataSource方法, 清理注冊的新數(shù)據(jù)源。

public class DynamicDataSource extends AbstractRoutingDataSource {
  
      .................
      .................
      .................
  public void removeDataSource(String key){
    this.backupTargetDataSources.remove(key);
    super.setTargetDataSources(this.backupTargetDataSources);
    super.afterPropertiesSet();
  }
  
      .................
      .................
      .................
}

四種方式對比 

 

 

  分包方式 參數(shù)化切換 注解方式 動態(tài)添加方式
適用場景 編碼時便知道用哪個數(shù)據(jù)源 運行時才能確定用哪個數(shù)據(jù)源 編碼時便知道用哪個數(shù)據(jù)源 運行時動態(tài)添加新數(shù)據(jù)源
切換模式 自動 手動 自動 手動
mapper路徑 需要分包 無要求 無要求 無要求
增加數(shù)據(jù)源是否需要修改配置類 需要 不需要 不需要  
實現(xiàn)復雜度 簡單 復雜 復雜 復雜

 

事務問題

 

使用上述數(shù)據(jù)源配置方式,可實現(xiàn)單個數(shù)據(jù)源事務控制。

例如在一個service方法中,需要操作多個數(shù)據(jù)源執(zhí)行CUD時,是可以實現(xiàn)單個數(shù)據(jù)源事務控制的。方式如下,分別將需要事務控制的方法單獨抽取到另一個service,可實現(xiàn)單個事務方法的事務控制。

ServiceA:

public void updateMuilty(){
   serviceB.updateDb1();
   serviceB.updateDb2();
}

ServiceB:

@UsingDataSource(dataSourceId = "slave1")
@Transactional
public void updateDb1(){
  //業(yè)務邏輯......
}

@UsingDataSource(dataSourceId = "slave2")
@Transactional
public void updateDb2(){
  //業(yè)務邏輯......
}

但是在同一個方法里控制多個數(shù)據(jù)源的事務就不是這么簡單了,這就屬于分布式事務的范圍,可以考慮使用atomikos開源項目實現(xiàn)JTA分布式事務處理或者阿里的Fescar框架。

由于涉及到分布式事務控制,實現(xiàn)比較復雜,這里只是引出這個問題,后面抽時間把這塊補上來。

到此這篇關于Springcloud+Mybatis使用多數(shù)據(jù)源的四種方式(小結)的文章就介紹到這了,更多相關Springcloud Mybatis多數(shù)據(jù)源內容請搜索服務器之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持服務器之家!

原文鏈接:https://juejin.im/post/6875111962733182990

延伸 · 閱讀

精彩推薦
  • JAVA教程詳解java中的byte類型

    詳解java中的byte類型

    Java也提供了一個byte數(shù)據(jù)類型,并且是基本類型。java byte是做為最小的數(shù)字來處理的,因此它的值域被定義為-128~127,也就是signed byte。下面這篇文章主要給...

    夜有所思,日有所夢4412020-08-18
  • JAVA教程Java中使用json與前臺Ajax數(shù)據(jù)交互的方法

    Java中使用json與前臺Ajax數(shù)據(jù)交互的方法

    這篇文章主要為大家詳細介紹了Java中使用json與前臺Ajax數(shù)據(jù)交互的方法,分享Ajax獲取顯示Json數(shù)據(jù)的一種方法,感興趣的小伙伴們可以參考一下 ...

    海潛1652020-05-14
  • JAVA教程關于Java集合框架面試題(含答案)下

    關于Java集合框架面試題(含答案)下

    Java集合框架為Java編程語言的基礎,也是Java面試中很重要的一個知識點。這里,我列出了一些關于Java集合的重要問題和答案。 ...

    cricode2122020-03-11
  • JAVA教程解決java后臺登錄前后cookie不一致問題

    解決java后臺登錄前后cookie不一致問題

    本文主要介紹了java后臺登錄前后cookie不一致的解決方案,具有很好的參考價值,需要的朋友一起來看下吧...

    胡金水4672020-07-17
  • JAVA教程Java集合和數(shù)組的區(qū)別

    Java集合和數(shù)組的區(qū)別

    本文主要介紹了Java集合和數(shù)組的區(qū)別。具有很好的參考價值,下面跟著小編一起來看下吧...

    夏日的微笑2802020-08-04
  • JAVA教程Java模版引擎Freemarker

    Java模版引擎Freemarker

    FreeMarker是一個模板引擎,一個基于模板生成文本輸出的通用工具,使用純Java編寫 FreeMarker被設計用來生成HTML Web頁面,特別是基于MVC模式的應用程序 ...

    JocelynJiao1912020-04-21
  • JAVA教程詳解Java實現(xiàn)緩存(LRU,FIFO)

    詳解Java實現(xiàn)緩存(LRU,FIFO)

    本篇文章主要介紹了詳解Java實現(xiàn)緩存(LRU,FIFO) ,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧 ...

    liuyang03782020-09-06
  • JAVA教程springmvc攔截器登錄驗證示例

    springmvc攔截器登錄驗證示例

    本篇文章主要介紹了springmvc攔截器登錄驗證示例,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧 ...

    書劍江山1582020-08-21
主站蜘蛛池模板: 俄罗斯烧性春三级k8播放 | 国产综合成人久久大片91 | 99草视频 | 艹b视频在线观看 | 国产露脸对白刺激3p在线 | 香蕉久久网 | 97久久天天综合色天天综合色hd | 久久伊人精品青青草原2021 | 亚洲 欧美 国产 在线观看 | 四虎免费在线观看 | 二次元美女挤奶漫画 | 国产a一级 | 日韩精品亚洲专区在线影视 | 成人国产精品一区二区不卡 | zoomkool最新版 | 小小水蜜桃3视频在线观看 小鸟酱喷水 | 2020年最新国产精品视频免费 | 国产成人精品视频一区二区不卡 | 小柔的性放荡羞辱日记动漫 | xnxx老师| 五月天精品视频在线观看 | 国产小嫩模好紧 | yy111111影院理论大片 | 亚洲国产成人99精品激情在线 | 亚洲免费在线视频 | 精品一区二区三区在线视频观看 | 操熟美女又肥又嫩的骚屁股 | 欧美日韩在线观看一区二区 | 美女被狂揉下部羞羞动漫 | 国产成人精品系列在线观看 | 国产高清精品自在久久 | 五月色婷婷在线影院 | 牛人国产偷窥女洗浴在线观看 | 吃大胸寡妇的奶 | 亚洲国产第一区二区香蕉日日 | 色视频久久 | 国产馆在线观看免费的 | 欧美在线观看一区二区三 | 18hdxxxx日本护士| 桃色公寓 | 成人中文字幕在线高清 |