天天看點

SpringBoot 實作 Oracle 主從資料庫的動态切換,并實作讀寫分離

1、問題提出

目前 Oracle 中有兩個資料庫,要實作一個資料庫隻進行讀操作,另一個資料庫進行寫操作,也即資料庫的讀寫分離,該怎麼做?

2、簡要說明:本問題與【主從複制、讀寫分離】還不太一樣

主從複制、讀寫分離一般是一起使用的。目的很簡單,就是為了提高資料庫的并發性能。你想,假設是單機,讀寫都在一台 MySQL 上面完成,性能肯定不高。如果有三台MySQL,一台 mater 隻負責寫操作,兩台 salve 隻負責讀操作,性能不就能大大提高了嗎?

是以主從複制、讀寫分離就是為了資料庫能支援更大的并發。

随着業務量的擴充,如果是單機部署的 MySQL,會導緻I/O頻率過高。采用主從複制、讀寫分離可以提高資料庫的可用性。

而本問題隻需要實作主從資料庫的動态切換即可。

3、AbstractRoutingDataSource

SpringBoot 提供了 AbstractRoutingDataSource,可以根據使用者自定義的規則選擇目前的資料源,這樣我們每次通路資料庫之前,設定要使用的資料源,就可以實作資料源的動态切換。

4、具體實作

1、建立一個類繼承 AbstractRoutingDataSource

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * 1. 建立 RoutingDataSource 繼承 AbstractRoutingDataSource
 * 重寫 determineCurrentLookupKey方法,傳回要使用的資料源key值。
 */
public class RoutingDataSource extends AbstractRoutingDataSource {
    private Logger logger = LogManager.getLogger();

    @Override
    protected Object determineCurrentLookupKey() {
        String dataSource = RoutingDataSourceHolder.getDataSource();
        logger.info("使用資料源:{}", dataSource);
        return dataSource;
    }
}
           
2、建立一個管理資料源 key 值的類,RoutingDataSourceManager
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

/**
 * 2. 建立一個管理資料源key值的類 RoutingDataSourceHolder
 * 代碼設定了一個事務内使用同一個資料源。
 */
public class RoutingDataSourceManager {

    private static Logger logger = LogManager.getLogger();

    private static final ThreadLocal<String> dataSources = new ThreadLocal<>();

    // 一個事務内使用同一個資料源
    public static void setDataSource(String dataSourceName) {
        if (dataSources.get() == null) {
            dataSources.set(dataSourceName);
            logger.info("設定資料源:{}", dataSourceName);
        }
    }

    public static String getDataSource() {
        return dataSources.get();
    }

    public static void clearDataSource() {
        dataSources.remove();
    }

}
           

3、application.properties

# OracleDbProperties  
# master dbsource
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=oracle.jdbc.driver.OracleDriver
spring.datasource.url=jdbc:oracle:thin:@xxx.xxx.xxx.7:1521:xxx
spring.datasource.username=xxx
spring.datasource.password=xxx

# slave dbsource
spring.slave-datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.slave-datasource.driver-class-name=oracle.jdbc.driver.OracleDriver
spring.slave-datasource.url=jdbc:oracle:thin:@xxx.xxx.xxx.6:1521:xxx
spring.slave-datasource.username=xxx
spring.slave-datasource.password=xxx
           

4、配置主從資料庫

import com.alibaba.druid.pool.DruidDataSource;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * 配置主從資料庫:主:xxx.xxx.xxx.7;從:xxx.xxx.xxx.6
 */
@Configuration
public class DataSourceConfigurer {
    private Logger logger = LogManager.getLogger();

    public final static String MASTER_DATASOURCE = "masterDataSource";// 主資料庫
    public final static String SLAVE_DATASOURCE = "slaveDataSource";// 從資料庫

    // 主資料庫:.7
    @Bean(MASTER_DATASOURCE)
    @ConfigurationProperties(prefix = "spring.datasource")
    public DruidDataSource masterDataSource(DataSourceProperties properties) {
        DruidDataSource build = properties.initializeDataSourceBuilder().type(DruidDataSource.class).build();
        logger.info("配置主資料庫:{}", build);
        return build;
    }

    // 從資料庫:.6
    @Bean(SLAVE_DATASOURCE)
    @ConfigurationProperties(prefix = "spring.slave-datasource")
    public DruidDataSource slaveDataSource(DataSourceProperties properties) {
        DruidDataSource build = properties.initializeDataSourceBuilder().type(DruidDataSource.class).build();
        logger.info("配置從資料庫:{}", build);
        return build;
    }

    /**
     * Primary   優先使用該Bean
     * DependsOn 先執行主從資料庫的配置
     * Qualifier 指定使用哪個Bean
     *
     * @param masterDataSource 主資料源
     * @param slaveDataSource  從資料源
     * @return
     */
    @Bean
    @Primary
    @DependsOn(value = {MASTER_DATASOURCE, SLAVE_DATASOURCE})
    public DataSource routingDataSource(@Qualifier(MASTER_DATASOURCE) DruidDataSource masterDataSource,
                                        @Qualifier(SLAVE_DATASOURCE) DruidDataSource slaveDataSource) {
        if (StringUtils.isBlank(slaveDataSource.getUrl())) {
            logger.info("沒有配置從資料庫,預設使用主資料庫");
            return masterDataSource;
        }

        // 設定初始化targetDataSources對象
        Map<Object, Object> map = new HashMap<>();
        map.put(DataSourceConfigurer.MASTER_DATASOURCE, masterDataSource);
        map.put(DataSourceConfigurer.SLAVE_DATASOURCE, slaveDataSource);
        RoutingDataSource routing = new RoutingDataSource();
        // 設定動态資料源
        routing.setTargetDataSources(map);
        // 設定預設資料源
        routing.setDefaultTargetDataSource(masterDataSource);
        logger.info("主從資料庫配置完成");
        return routing;
    }
}
           

5、自定義注解和切面類

自定義注解:

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)// 聲明注解有效的時間
@Target({ElementType.TYPE, ElementType.METHOD})// 說明該注解可以寫在類和方法上
@Documented
public @interface DataSourceWith {
    String key() default "";
}
           

切面類:

<!-- 添加aop依賴 -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
           
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Aspect
@Order(-1)// 保證該AOP在@Transactional之前運作
@Component
public class DataSourceWithAspect {

    /**
     * 使用DataSourceWith注解就攔截
     */
    @Pointcut("@annotation(cn.edu.zzuli.hnsmz.annotation.DataSourceWith)||@within(cn.edu.zzuli.hnsmz.annotation.DataSourceWith)")
    public void doPointcut() {

    }

    /**
     * 方法前,為了在事務前設定
     */
    @Before("doPointcut()")
    public void doBefore(JoinPoint joinPoint) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        // 擷取注解對象
        DataSourceWith dataSource = method.getAnnotation(DataSourceWith.class);
        if (dataSource == null) {
            // 方法沒有就擷取類上的
            dataSource = method.getDeclaringClass().getAnnotation(DataSourceWith.class);
        }
        String key = dataSource.key();
        RoutingDataSourceHolder.setDataSource(key);
    }

    @After("doPointcut()")
    public void doAfter(JoinPoint joinPoint) {
        RoutingDataSourceHolder.clearDataSource();
    }

}
           
6、使用
@DataSourceWith(key = DataSourceConfigurer.SLAVE_DATASOURCE)
public Long selectById(String id) {
    return studentService.selectById(id);
}
           

參考自:https://blog.csdn.net/m0_68615056/article/details/123738282