首頁 > 軟體

SpringBoot實現多資料來源的切換實踐

2022-03-03 19:02:05

前言

我們在進行軟體開發的過程中,剛開始的時候因為無法估量系統後期的存取量和並行量,所以一開始會採用單體架構,後期如果網站流量變大, 並行量變大,那麼就可能會將架構擴充套件為微服務架構,各個微服務對應一個資料庫,不過這樣的成本就有點大了,可能只是有些模組用的人比較多, 有些模組沒什麼人用,如果都進行服務拆分,其實也沒那個必要,如果有些模組用的人比較多,那麼我們可以採用讀寫分離來減輕壓力,這樣的話, 可以在一定程度上提升系統的使用者體驗,不過這只是在資料庫的I/O上面做方案,如果系統的壓力很大,那麼肯定要做負載均衡,我們今天就先說 實現資料庫的讀寫分離。我們要在程式碼層面實現資料庫的讀寫分離,那麼核心就是資料來源的切換,本文基於AOP來實現資料來源的切換。

工程結構

└─com
    └─steak
        └─transaction
            │  TransactionDemoApplication.java
            │  
            ├─datasource
            │  │  DatasourceChooser.java
            │  │  DatasourceConfiguration.java
            │  │  DatasourceContext.java
            │  │  DatasourceScope.java
            │  │  DynamicDatasourceAspect.java
            │  │  
            │  └─properties
            │          DynamicDatasourceProperties.java
            │          MainDatasourceProperties.java
            │          
            ├─execute
            │      PlaceOrderExecute.java
            │      
            ├─rest
            │      PlaceOrderApi.java
            │      
            ├─result
            │      R.java
            │      
            └─service
                    IntegralService.java
                    OrderService.java
                    StockService.java

在下面的實現中,我們一個有三個資料來源,其中有一個是預設的,如果不指定具體的資料來源,那麼就使用預設的,我們是基於申明式的方式來切換 資料來源,只需要在具體的介面上加上註解便能實現資料來源的切換。

編碼實現

yml檔案

主資料來源直接使用spring的設定,其他資料來源採用自定義的方式,這裡採用一個map結構來定義,方便進行解析,可以在yml檔案裡進行新增多個,在程式碼 邏輯層面不用改動。

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      username: root
      password: 123456
      url: jdbc:mysql://127.0.0.1:3306/db?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC
      driver-class-name: com.mysql.cj.jdbc.Driver

dynamic:
  datasource: {
    slave1: {
      username: 'root',
      password: '123456',
      url: ' url: jdbc:mysql://127.0.0.1:3306/db?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC',
      driver-class-name: 'com.mysql.cj.jdbc.Driver'
    },
    slave2: {
      username: 'root',
      password: '123456',
      url: ' url: jdbc:mysql://127.0.0.1:3306/db?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC',
      driver-class-name: 'com.mysql.cj.jdbc.Driver'
    }
  }

主資料來源MainDatasourceProperties

對於主資料來源,我們單獨拿出來放在一個類裡面,不過其實也可以放到dynamic裡面,只是需要做一定的處理,我們就簡單的放幾個連線屬性。

/**
 * @author 劉牌
 * @date 2022/3/220:14
 */
@Component
@ConfigurationProperties(prefix = "spring.datasource.druid")
@Data
public class MainDatasourceProperties {
    private String username;
    private String password;
    private String url;
    private String driverClassName;
}

其他資料來源DynamicDatasourceProperties

其他資料來源使用一個Map來接受yml檔案中的資料來源設定

/**
 * @author 劉牌
 * @date 2022/3/213:47
 */
@Component
@ConfigurationProperties(prefix = "dynamic")
@Data
public class DynamicDatasourceProperties {
    private Map<String,Map<String,String>> datasource;
}

資料來源設定類DatasourceConfiguration

設定類中主要對DataSource進行設定,主資料來源我們按照正常的bean來定義連線屬性,而其他資料資料來源則使用反射的方式來進行連線屬性的 設定,因為主資料來源一般是不會變動的,但是其他資料來源可能會發生變動,可能會新增,這時候如果通過寫死取設定,那麼每增加一個資料來源,就需要 增加一個設定,顯然不太行,所以就使用反射來進行賦值。

/**
 * @author 劉牌
 * @date 2022/3/111:34
 */
@Configuration
@AllArgsConstructor
public class DatasourceConfiguration {

    final MainDatasourceProperties mainDatasourceProperties;
    final DynamicDatasourceProperties dynamicDatasourceProperties;

    @Bean
    public DataSource datasource(){
        Map<Object,Object> datasourceMap = new HashMap<>();
        DatasourceChooser datasourceChooser = new DatasourceChooser();
        /**
         * main database
         */
        DruidDataSource mainDataSource = new DruidDataSource();
        mainDataSource.setUsername(mainDatasourceProperties.getUsername());
        mainDataSource.setPassword(mainDatasourceProperties.getPassword());
        mainDataSource.setUrl(mainDatasourceProperties.getUrl());
        mainDataSource.setDriverClassName(mainDatasourceProperties.getDriverClassName());
        datasourceMap.put("main",mainDataSource);
        /**
         * other database
         */
        Map<String, Map<String, String>> sourceMap = dynamicDatasourceProperties.getDatasource();
        sourceMap.forEach((datasourceName,datasourceMaps) -> {
            DruidDataSource dataSource = new DruidDataSource();
            datasourceMaps.forEach((K,V) -> {
                String setField = "set" + K.substring(0, 1).toUpperCase() + K.substring(1);
                //轉換yml檔案中帶有-符號的屬性
                String[] strings = setField.split("");
                StringBuilder newStr = new StringBuilder();
                for (int i = 0; i < strings.length; i++) {
                    if (strings[i].equals("-")) strings[i + 1] = strings[i + 1].toUpperCase();
                    if (!strings[i].equals("-")) newStr.append(strings[i]);
                }
                try {
                    DruidDataSource.class.getMethod(newStr.toString(),String.class).invoke(dataSource,V);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
            datasourceMap.put(datasourceName,dataSource);
        });
        //設定目標資料來源
        datasourceChooser.setTargetDataSources(datasourceMap);
        //設定預設資料來源
        datasourceChooser.setDefaultTargetDataSource(mainDataSource);
        return datasourceChooser;
    }
}

上面使用資料來源設定類中使用反射對其他資料來源進行連線屬性的設定,然後設定目標資料來源和預設資料來源,裡面有一個DatasourceChooser

DatasourceChooser

DatasourceChooser繼承自AbstractRoutingDataSourceAbstractRoutingDataSource可以實現資料來源的切換,它裡面的 determineCurrentLookupKey()方法需要我們返回一個資料來源的名稱,它會自動給我們匹配上資料來源。

/**
 * @author 劉牌
 * @date 2022/3/112:21
 */
public class DatasourceChooser extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DatasourceContext.getDatasource();
    }
}

如下是AbstractRoutingDataSource的部分原始碼,我們可以看出資料來源是一個Map結構,可以通過資料來源名稱查詢到對應的資料來源。

package org.springframework.jdbc.datasource.lookup;

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
    @Nullable
    private Map<Object, DataSource> resolvedDataSources;
    public AbstractRoutingDataSource() {
    }
    protected DataSource determineTargetDataSource() {
        Object lookupKey = this.determineCurrentLookupKey();
        DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
    }

    @Nullable
    protected abstract Object determineCurrentLookupKey();
}

DatasourceContext

DatasourceContext內部是一個ThreadLocal,主要是用來儲存每一個執行緒的資料來源名稱和獲取資料來源名稱,而資料來源的名稱我們用過AOP切面 來獲取。

/**
 * @author 劉牌
 * @date 2022/3/112:22
 */
public class DatasourceContext {
    private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
    public static void setDatasource(String key){
        threadLocal.set(key);
    }
    public static String getDatasource(){
        return threadLocal.get();
    }
}

資料來源註解DatasourceScope

DatasourceScope標準在方法上面,通過scope來指定資料來源,不指定預設為主資料來源main

/**
 * @author 劉牌
 * @date 2022/3/111:22
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DatasourceScope {
    String scope() default "main";
}

資料來源切面DynamicDatasourceAspect

我們在存取每一個帶有DatasourceScope註解的方法時,都會經過資料來源切面DynamicDatasourceAspect,獲取到註解上面的 scope的值後,通過DatasourceContext設定資料來源名稱,便可實現對資料來源的切換。

/**
 * @author 劉牌
 * @date 2022/3/111:34
 */
@Aspect
@Component
public class DynamicDatasourceAspect {

    @Pointcut("@annotation(dataSourceScope)")
    public void dynamicPointcut(DatasourceScope dataSourceScope){}

    @Around(value = "dynamicPointcut(dataSourceScope)", argNames = "joinPoint,dataSourceScope")
    public Object dynamicAround(ProceedingJoinPoint joinPoint , DatasourceScope dataSourceScope) throws Throwable {
        String scope = dataSourceScope.scope();
        DatasourceContext.setDatasource(scope);
        return joinPoint.proceed();
    }
}

使用

只需要在具體的方法上面標註資料來源註解@DatasourceScope,並指定scope的值,便可實現切換,如果不指定,那麼就使用主資料來源。

/**
 * @author 劉牌
 * @date 2022/3/19:49
 */
@Service
@AllArgsConstructor
public class OrderService {
    
    private JdbcTemplate jdbcTemplate;
    
    @DatasourceScope(scope = "slave1")
    public R saveOrder(Integer userId , Integer commodityId){
        String sql = "INSERT INTO `order`(user_id,commodity_id) VALUES("+userId+","+commodityId+")";
        jdbcTemplate.execute(sql);
        return R.builder().code(200).msg("save order success").build();
    }
}

到此這篇關於SpringBoot實現多資料來源的切換實踐的文章就介紹到這了,更多相關SpringBoot多資料來源切換內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


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