首頁 > 軟體

Springcloud+Mybatis使用多資料來源的四種方式(小結)

2020-09-23 15:03:31

前段時間在做會員中心和中介軟體系統開發時,多次碰到多資料來源設定問題,主要用到分包方式、引數化切換、註解+AOP、動態新增 這四種方式。這裡做一下總結,分享下使用心得以及踩過的坑。

分包方式

資料來源組態檔

在yml中,設定兩個資料來源,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

資料來源設定類

 master資料來源設定類

注意點:

需要用@Primary註解指定預設資料來源,否則spring不知道哪個是主資料來源;

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

  //預設資料來源
  @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資料來源設定類

@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檔案,需要按照不同的資料來源分包。在運算元據庫時,根據需要在service類中注入dao層。

特點分析

優點

實現起來簡單,只需要編寫資料來源組態檔和設定類,mapper介面和xml檔案注意分包即可。

缺點

很明顯,如果後面要增加或刪除資料來源,不僅要修改資料來源組態檔,還需要修改設定類。

例如增加一個資料來源,同時還需要新寫一個該資料來源的設定類,同時還要考慮新建mapper介面包、xml包等,沒有實現 「熱插拔」 效果。

引數化切換方式

 思想

引數化切換資料來源,意思是,業務側需要根據當前業務引數,動態的切換到不同的資料來源。

這與分包思想不同。分包的前提是在編寫程式碼的時候,就已經知道當前需要用哪個資料來源,而引數化切換資料來源需要根據業務引數決定用哪個資料來源。

例如,請求引數userType值為1時,需要切換到資料來源slave1;請求引數userType值為2時,需要切換到資料來源slave2。

/**虛擬碼**/
int userType = reqUser.getType();
if (userType == 1){
  //切換到資料來源slave1
  //資料庫操作
} else if(userType == 2){
  //切換到資料來源slave2
  //資料庫操作
}

設計思路

 資料來源註冊

資料來源設定類建立datasource時,從yml組態檔中讀取所有資料來源設定,自動建立每個資料來源,並註冊至bean工廠和AbstractRoutingDatasource(後面聊聊這個),同時返回預設的資料來源master。

 資料來源切換

(1)通過執行緒池處理請求,每個請求獨佔一個執行緒,這樣每個執行緒切換資料來源時互不影響。

(2)根據業務引數獲取應切換的資料來源ID,根據ID從資料來源快取池獲取資料來源bean;

(3)生成當前執行緒資料來源key;

(4)將key設定到threadLocal;

(5)將key和資料來源bean放入資料來源快取池;

(6)在執行mapper方法前,spring會呼叫determineCurrentLookupKey方法獲取key,然後根據key去資料來源快取池取出資料來源,然後getConnection獲取該資料來源連線;

(7)使用該資料來源執行資料庫操作;

(8)釋放當前執行緒資料來源。

AbstractRoutingDataSource原始碼分析

spring為我們提供了AbstractRoutingDataSource抽象類,該類就是實現動態切換資料來源的關鍵。

我們看下該類的類圖,其實現了DataSource介面。

我們看下它的getConnection方法的邏輯,其首先呼叫determineTargetDataSource來獲取資料來源,再獲取資料庫連線。很容易猜想到就是這裡來決定具體使用哪個資料來源的。

進入到determineTargetDataSource方法,我們可以看到它先是呼叫determineCurrentLookupKey獲取到一個lookupKey,然後根據這個key去resolvedDataSources裡去找相應的資料來源。

看下該類定義的幾個物件,defaultTargetDataSource是預設資料來源,resolvedDataSources是一個map物件,儲存所有主從資料來源。

所以,關鍵就是這個lookupKey的獲取邏輯,決定了當前獲取的是哪個資料來源,然後執行getConnection等一系列操作。determineCurrentLookupKey是AbstractRoutingDataSource類中的一個抽象方法,而它的返回值是你所要用的資料來源dataSource的key值,有了這個key值,resolvedDataSource(這是個map,由組態檔中設定好後存入的)就從中取出對應的DataSource,如果找不到,就用設定預設的資料來源。

所以,通過擴充套件AbstractRoutingDataSource類,並重寫其中的determineCurrentLookupKey()方法,可以實現資料來源的切換。

程式碼實現

下面貼出關鍵程式碼實現。

資料來源組態檔

這裡配了3個資料來源,其中主資料來源是MySQL,兩個從資料來源是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

定義動態資料來源

主要是繼承AbstractRoutingDataSource,實現determineCurrentLookupKey方法。

public class DynamicDataSource extends AbstractRoutingDataSource {
  /*儲存所有資料來源*/
  private Map<Object, Object> backupTargetDataSources;

  public Map<Object, Object> getBackupTargetDataSources() {
    return backupTargetDataSources;
  }
  /*defaultDataSource為預設資料來源*/
  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();
  }
  /*返回當前執行緒的資料來源的key*/
  @Override
  protected Object determineCurrentLookupKey() {
    return DynamicDataSourceContextHolder.getContextKey();
  }
}

定義資料來源key執行緒變數持有

定義一個ThreadLocal靜態變數,該變數持有了執行緒和執行緒的資料來源key之間的關係。當我們要切換資料來源時,首先要自己生成一個key,將這個key存入threadLocal執行緒變數中;同時還應該從DynamicDataSource物件中的backupTargetDataSources屬性中獲取到資料來源物件, 然後將key和資料來源物件再put到backupTargetDataSources中。 這樣,spring就能根據determineCurrentLookupKey方法返回的key,從backupTargetDataSources中取出我們剛剛設定的資料來源物件,進行getConnection等一系列操作了。

public class DynamicDataSourceContextHolder {
  /**
   * 儲存執行緒和資料來源key的對映關係
   */
  private static final ThreadLocal<String> DATASOURCE_CONTEXT_KEY_HOLDER = new ThreadLocal<>();

  /***
   * 設定當前執行緒資料來源key
   */
  public static void setContextKey(String key) {
    DATASOURCE_CONTEXT_KEY_HOLDER.set(key);
  }
  /***
   * 獲取當前執行緒資料來源key
   */
  public static String getContextKey() {
    String key = DATASOURCE_CONTEXT_KEY_HOLDER.get();
    return key == null ? DataSourceConstants.DS_KEY_MASTER : key;
  }
  /***
   * 刪除當前執行緒資料來源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();
  }
}

多資料來源自動設定類

這裡通過讀取yml組態檔中所有資料來源的設定,自動為每個資料來源建立datasource 物件並註冊至bean工廠。同時將這些資料來源物件,設定到AbstractRoutingDataSource中。

通過這種方式,後面如果需要新增或修改資料來源,都無需新增或修改java設定類,只需去設定中心修改yml檔案即可。

@Configuration
@MapperScan(basePackages = "com.hosjoy.xxx.xxx.modules.xxx.mapper")
public class DynamicDataSourceConfig {
  @Autowired
  private BeanFactory beanFactory;
  @Autowired
  private DynamicDataSourceProperty dynamicDataSourceProperty;
  /**
   * 功能描述: <br>
   * 〈動態資料來源bean 自動設定註冊所有資料來源〉
   *
   * @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所有資料來源設定*/
    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()) {
        //建立資料來源物件
        HikariDataSource dataSource = (HikariDataSource) DataSourceBuilder.create().build();
        String dataSourceId = entry.getKey();
        configeDataSource(entry, dataSource);
        /*bean工廠註冊每個資料來源bean*/
        listableBeanFactory.registerSingleton(dataSourceId, dataSource);
        dataSourceMap.put(dataSourceId, dataSource);
      }
    });
    //AbstractRoutingDataSource設定主從資料來源
    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"));
  }

}

資料來源切換工具類

切換邏輯:

(1)生成當前執行緒資料來源key

(2)根據業務條件,獲取應切換的資料來源ID;

(3)根據ID從資料來源快取池中獲取資料來源物件,並再次新增到backupTargetDataSources快取池中;

(4)threadLocal設定當前執行緒對應的資料來源key;

(5)在執行資料庫操作前,spring會呼叫determineCurrentLookupKey方法獲取key,然後根據key去資料來源快取池取出資料來源,然後getConnection獲取該資料來源連線;

(6)使用該資料來源執行資料庫操作;

(7)釋放快取:threadLocal清理當前執行緒資料來源資訊、資料來源快取池清理當前執行緒資料來源key和資料來源物件,目的是防止記憶體漏失。

@Slf4j
@Component
public class DataSourceUtil {
  @Autowired
  private DataSourceConfiger dataSourceConfiger;
  
  /*根據業務條件切換資料來源*/
  public void switchDataSource(String key, Predicate<? super Map<String, Object>> predicate) {
    try {
      //生成當前執行緒資料來源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");
      //根據ID從資料來源快取池中獲取資料來源物件,並再次新增到backupTargetDataSources
      addDataSource(newDsKey, id);
      //設定當前執行緒對應的資料來源key
      DynamicDataSourceContextHolder.setContextKey(newDsKey);
      log.info("當前執行緒資料來源切換成功,當前資料來源ID:{}", id);

    }
    catch (Exception e) {
      log.error("切換資料來源失敗,請檢查資料來源組態檔。key:{}, 條件:{}", key, predicate.toString());
      throw new ClientException("切換資料來源失敗,請檢查資料來源設定", e);
    }
  }
  
  /*將資料來源新增至多資料來源快取池中*/
  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 =.........;
  //切換資料來源
  dataSourceUtil.switchDataSource("testKey", predicate);
  //資料庫操作
  mapper.testQuery();
  //清除快取,避免記憶體漏失
  DynamicDataSourceContextHolder.removeContextKey();
}

每次資料來源使用後,都要呼叫removeContextKey方法清除快取,避免記憶體漏失,這裡可以考慮用AOP攔截特定方法,利用後置通知為執行方法代理執行快取清理工作。

@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("方法執行完畢,開始清空當前執行緒資料來源,執行緒id:{},代理方法:{}",id,name);
    DynamicDataSourceContextHolder.removeContextKey();
    log.info("當前執行緒資料來源清空完畢,已返回至預設資料來源:{}",id);
  }
}

特點分析

(1)引數化切換資料來源方式,出發點和分包方式不一樣,適合於在執行時才能確定用哪個資料來源。

(2)需要手動執行切換資料來源操作;

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

(4)增加資料來源,無需修改java設定類,只需修改資料來源組態檔即可。

註解方式

思想

該方式利用註解+AOP思想,為需要切換資料來源的方法標記自定義註解,註解屬性指定資料來源ID,然後利用AOP切面攔截註解標記的方法,在方法執行前,切換至相應資料來源;在方法執行結束後,切換至預設資料來源。

需要注意的是,自定義切面的優先順序需要高於@Transactional註解對應切面的優先順序。

否則,在自定義註解和@Transactional同時使用時,@Transactional切面會優先執行,切面在呼叫getConnection方法時,會去呼叫AbstractRoutingDataSource.determineCurrentLookupKey方法,此時獲取到的是預設資料來源master。這時@UsingDataSource對應的切面即使再設定當前執行緒的資料來源key,後面也不會再去呼叫determineCurrentLookupKey方法來切換資料來源了。

設計思路

資料來源註冊

同上。

資料來源切換

利用切面,攔截所有@UsingDataSource註解標記的方法,根據dataSourceId屬性,在方法執行前,切換至相應資料來源;在方法執行結束後,清除快取並切換至預設資料來源。

程式碼實現

資料來源組態檔

同上。

定義動態資料來源

同上。

定義資料來源key執行緒變數持有

同上。

多資料來源自動設定類

同上。

資料來源切換工具類

切換邏輯:

(1)生成當前執行緒資料來源key

(3)根據ID從資料來源快取池中獲取資料來源物件,並再次新增到backupTargetDataSources快取池中;

(4)threadLocal設定當前執行緒對應的資料來源key;

(5)在執行資料庫操作前,spring會呼叫determineCurrentLookupKey方法獲取key,然後根據key去資料來源快取池取出資料來源,然後getConnection獲取該資料來源連線;

(6)使用該資料來源執行資料庫操作;

(7)釋放快取:threadLocal清理當前執行緒資料來源資訊、資料來源快取池清理當前執行緒資料來源key和資料來源物件。

public static void switchDataSource(String dataSourceId) {
  if (StringUtils.isBlank(dataSourceId)) {
    throw new ClientException("切換資料來源失敗,資料來源ID不能為空");
  }
  try {
    String threadDataSourceKey = UUID.randomUUID().toString();
    DataSourceUtil.addDataSource(threadDataSourceKey, dataSourceId);
    DynamicDataSourceContextHolder.setContextKey(threadDataSourceKey);
  }
  catch (Exception e) {
    log.error("切換資料來源失敗,未找到指定的資料來源,請確保所指定的資料來源ID已在組態檔中設定。dataSourceId:{}", dataSourceId);
    throw new ClientException("切換資料來源失敗,未找到指定的資料來源,請確保所指定的資料來源ID已在組態檔中設定。dataSourceId:" + dataSourceId, e);
  }
}

自定義註解

自定義註解標記當前方法所使用的資料來源,預設為主資料來源。

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

  String dataSourceId() default "master";
}

切面

主要是定義前置通知和後置通知,攔截UsingDataSource註解標記的方法,方法執行前切換資料來源,方法執行後清理資料來源快取。

需要標記切面的優先順序比@Transaction註解對應切面的優先順序要高。否則,在自定義註解和@Transactional同時使用時,@Transactional切面會優先執行,切面在呼叫getConnection方法時,會去呼叫AbstractRoutingDataSource.determineCurrentLookupKey方法,此時獲取到的是預設資料來源master。這時@UsingDataSource對應的切面即使再設定當前執行緒的資料來源key,後面也不會再去呼叫determineCurrentLookupKey方法來切換資料來源了。

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

  //攔截UsingDataSource註解標記的方法,方法執行前切換資料來源
  @Before(value = "@annotation(usingDataSource)")
  public void before(JoinPoint joinPoint, UsingDataSource usingDataSource) {
    String dataSourceId = usingDataSource.dataSourceId();
    log.info("執行目標方法前開始切換資料來源,目標方法:{}, dataSourceId:{}", joinPoint.getSignature().toString(), dataSourceId);
    try {
      DataSourceUtil.switchDataSource(dataSourceId);
    }
    catch (Exception e) {
      log.error("切換資料來源失敗!資料來源可能未設定或不可用,資料來源ID:{}", dataSourceId, e);
      throw new ClientException("切換資料來源失敗!資料來源可能未設定或不可用,資料來源ID:" + dataSourceId, e);
    }
    log.info("目標方法:{} , 已切換至資料來源:{}", joinPoint.getSignature().toString(), dataSourceId);
  }

  //攔截UsingDataSource註解標記的方法,方法執行後清理資料來源,防止記憶體漏失
  @After(value = "@annotation(com.hosjoy.hbp.dts.common.annotation.UsingDataSource)")
  public void after(JoinPoint joinPoint) {
    log.info("目標方法執行完畢,執行清理,切換至預設資料來源,目標方法:{}", joinPoint.getSignature().toString());
    try {
      DynamicDataSourceContextHolder.removeContextKey();
    }
    catch (Exception e) {
      log.error("清理資料來源失敗", e);
      throw new ClientException("清理資料來源失敗", e);
    }
    log.info("目標方法:{} , 資料來源清理完畢,已返回預設資料來源", 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;
}

動態新增方式(非常用)

 業務場景描述

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

專案裡面只設定了1個預設的資料來源,而具體執行時需要動態的新增新的資料來源,非已設定好的靜態的多資料來源。例如需要去伺服器實時讀取資料來源設定資訊(非設定在本地),然後再執行資料庫操作。

這種業務場景,以上3種方式就都不適用了,因為上述的資料來源都是提前在yml檔案配製好的。

實現思路

除了第6步外,利用之前寫好的程式碼就可以實現。

思路是:

(1)建立新資料來源;

(2)DynamicDataSource註冊新資料來源;

(3)切換:設定當前執行緒資料來源key;新增臨時資料來源;

(4)資料庫操作(必須在另一個service實現,否則無法控制事務);

(5)清理當前執行緒資料來源key、清理臨時資料來源;

(6)清理剛剛註冊的資料來源;

(7)此時已返回至預設資料來源。

程式碼

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

Service A:

public String testUsingNewDataSource(){
    DynamicDataSource dynamicDataSource = RequestHandleMethodRegistry.getContext().getBean("dynamicDataSource", DynamicDataSource.class);
    try {
      //模擬從伺服器讀取資料來源資訊
      //..........................
      //....................
      
      //建立新資料來源
      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註冊新資料來源
      dynamicDataSource.addDataSource("test_ds_id", dataSource);

      //設定當前執行緒資料來源key、新增臨時資料來源
      DataSourceUtil.switchDataSource("test_ds_id");

      //資料庫操作(必須在另一個service實現,否則無法控制事務)
      serviceB.testInsert();
    }
    finally {
      //清理當前執行緒資料來源key
      DynamicDataSourceContextHolder.removeContextKey();

      //清理剛剛註冊的資料來源
      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("浦口區");
    po.setAddress("南京市浦口區寧六路219號");
    po.setDef(false);
    po.setCreateBy("23958");
    addressMapper.insert(po);
    //測試事務回滾
    int i = 1 / 0;
  }

DynamicDataSource: 增加removeDataSource方法, 清理註冊的新資料來源。

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

四種方式對比 

分包方式 引數化切換 註解方式 動態新增方式
適用場景 編碼時便知道用哪個資料來源 執行時才能確定用哪個資料來源 編碼時便知道用哪個資料來源 執行時動態新增新資料來源
切換模式 自動 手動 自動 手動
mapper路徑 需要分包 無要求 無要求 無要求
增加資料來源是否需要修改設定類 需要 不需要 不需要
實現複雜度 簡單 複雜 複雜 複雜

事務問題

使用上述資料來源設定方式,可實現單個資料來源事務控制。

例如在一個service方法中,需要操作多個資料來源執行CUD時,是可以實現單個資料來源事務控制的。方式如下,分別將需要事務控制的方法單獨抽取到另一個service,可實現單個事務方法的事務控制。

ServiceA:

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

ServiceB:

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

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

但是在同一個方法裡控制多個資料來源的事務就不是這麼簡單了,這就屬於分散式事務的範圍,可以考慮使用atomikos開源專案實現JTA分散式事務處理或者阿里的Fescar框架。

由於涉及到分散式事務控制,實現比較複雜,這裡只是引出這個問題,後面抽時間把這塊補上來。

到此這篇關於Springcloud+Mybatis使用多資料來源的四種方式(小結)的文章就介紹到這了,更多相關Springcloud Mybatis多資料來源內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com