1. 程式人生 > >Spring Boot 動態切換資料來源二——負載均衡

Spring Boot 動態切換資料來源二——負載均衡

如果僅僅是master-slave模式可以參考我前邊的文章Spring Boot HikariCP整合多資料來源
這篇文章也是在那個基礎上修改的,上篇文章中的多資料來源是有限制的,哪條sql使用哪個資料庫必須在程式碼中寫死。現在針對這點做優化,真正的整合多個數據源,且實現簡單的負載均衡。

相關主要程式碼
這裡寫圖片描述
先看配置檔案

slave:
  hosts: slave1,slave2
hikari:
  master:
    jdbc-url: jdbc:mysql://master_host:3306/mydb?useUnicode=true&characterEncoding=utf8&useSSL=true&allowMultiQueries=true&verifyServerCertificate=false
username: root password: root maximum-pool-size: 10 pool-name: master(localhost) connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1765000 data-source-properties: cachePrepStmts: true prepStmtCacheSize: 250 prepStmtCacheSqlLimit: 2048 useServerPrepStmts: true
useLocalSessionState: true useLocalTransactionState: true rewriteBatchedStatements: true cacheResultSetMetadata: true cacheServerConfiguration: true elideSetAutoCommits: true maintainTimeStats: false slave1: jdbc-url: jdbc:mysql://slave1_host:3306/mydb?useUnicode=true&characterEncoding=utf8&useSSL=true&allowMultiQueries=true&verifyServerCertificate=false
username: root password: root maximum-pool-size: 10 pool-name: slave1(localhost) connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1765000 read-only: true slave2: jdbc-url: jdbc:mysql://slave2_host:3306/mydb?useUnicode=true&characterEncoding=utf8&useSSL=true&allowMultiQueries=true&verifyServerCertificate=false username: root password: root maximum-pool-size: 10 pool-name: slave2(localhost) connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1765000 read-only: true

注:
1、slave下的data-source-properties:相關配置同master,這裡節省篇幅就省了,我這裡是公司的專案,沒有專門寫demo,所以程式碼不方便上傳git,只能貼出來了。
2、slave.hosts這裡配置是為了程式碼中簡單的負載均衡用的。

啟動類

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@Configuration
@MapperScan(basePackages="com.test.mapper")
public class AuthcenterApplication {

    public static void main(String[] args) {
        SpringApplication.run(AuthcenterApplication.class, args);
    }
}

注:exclude = DataSourceAutoConfiguration.class這裡是不讓啟動載入資料來源的,要不然啟動會報錯。

TargetDataSource

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author Created by pangkunkun on 2017/12/18.
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface TargetDataSource {
    //此處接收的是資料來源的名稱
    String value();
}

DBProperties

import com.zaxxer.hikari.HikariDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * @author Created by pangkunkun on 2017/12/18.
 */
@Component
@ConfigurationProperties(prefix = "hikari")
public class DBProperties {
    private HikariDataSource master;
    private HikariDataSource slave1;
    private HikariDataSource slave2;

   //省略getter和setter
}

DataSourceConfig

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.transaction.PlatformTransactionManager;

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

/**
 * @author Created by pangkunkun on 2017/12/18.
 */
@Configuration
public class DataSourceConfig {

    @Autowired
    private DBProperties properties;

    @Bean(name = "dataSource")
    public DataSource dataSource() {
        //按照目標資料來源名稱和目標資料來源物件的對映存放在Map中
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("master", properties.getMaster());
        targetDataSources.put("slave1", properties.getSlave1());
        targetDataSources.put("slave2", properties.getSlave2());
        //採用是想AbstractRoutingDataSource的物件包裝多資料來源
        DynamicDataSource dataSource = new DynamicDataSource();
        dataSource.setTargetDataSources(targetDataSources);
        //設定預設的資料來源,當拿不到資料來源時,使用此配置
        dataSource.setDefaultTargetDataSource(properties.getMaster());
        return dataSource;
    }

    @Bean
    public PlatformTransactionManager txManager() {
        return new DataSourceTransactionManager(dataSource());
    }
}

注: 這裡設定所有配置的資料庫

DataSourceAspect

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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Random;

/**
 * @author Created by pangkunkun on 2017/12/18.
 */
@Component
@Aspect
public class DataSourceAspect {

    private final static Logger log= LoggerFactory.getLogger(DataSourceAspect.class);

    @Value("${slave.hosts}")
    private String slaveHosts;

    //切換放在mapper介面的方法上,所以這裡要配置AOP切面的切入點
    @Pointcut("execution( * com.test.mapper.*.*(..))")
    public void dataSourcePointCut() {
    }

    @Before("dataSourcePointCut()")
    public void before(JoinPoint joinPoint) {
        Object target = joinPoint.getTarget();
        String method = joinPoint.getSignature().getName();
        Class<?>[] clazz = target.getClass().getInterfaces();
        Class<?>[] parameterTypes = ((MethodSignature) joinPoint.getSignature()).getMethod().getParameterTypes();
        try {
            Method m = clazz[0].getMethod(method, parameterTypes);
            //如果方法上存在切換資料來源的註解,則根據註解內容進行資料來源切換
            if (m != null && m.isAnnotationPresent(TargetDataSource.class)) {
                TargetDataSource data = m.getAnnotation(TargetDataSource.class);
                String dataSourceName = data.value();
                //判斷指定的資料來源型別,如果是slave,則呼叫LB方法,隨機分配slave資料庫
                if (dataSourceName.equals("slave")){
                    dataSourceName = slaveLoadBalance();
                }
                DynamicDataSourceHolder.putDataSource(dataSourceName);
                log.debug("current thread " + Thread.currentThread().getName() + " add " + dataSourceName + " to ThreadLocal");
            } else {
                log.debug("switch datasource fail,use default");
            }
        } catch (Exception e) {
            log.error("current thread " + Thread.currentThread().getName() + " add data to ThreadLocal error", e);
        }
    }

    //執行完切面後,將執行緒共享中的資料來源名稱清空
    @After("dataSourcePointCut()")
    public void after(JoinPoint joinPoint){
        DynamicDataSourceHolder.removeDataSource();
    }

    //自己實現的隨機指定slave資料來源的LB
    private  String slaveLoadBalance() {
        String[] slaves = slaveHosts.split(",");
        //通過隨機獲取陣列中資料庫的名稱來隨機分配要使用的資料庫
        int num = new Random().nextInt(slaves.length);
        return slaves[num];
    }
}

dataSourcePointCut這裡指定切面的生效範圍,這裡定義的是自己的mapper,我是在mybatis被呼叫的介面處指定資料來源的。

DynamicDataSourceHolder

/**
 * @author Created by pangkunkun on 2017/12/18.
 */
public class DynamicDataSourceHolder {

    /**
     * 本地執行緒共享物件
     */
    private static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();

    public static void putDataSource(String name) {
        THREAD_LOCAL.set(name);
    }

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

    public static void removeDataSource() {
        THREAD_LOCAL.remove();
    }
}

DynamicDataSource

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * @author Created by pangkunkun on 2017/12/18.
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    /**
     * 資料來源路由,此方用於產生要選取的資料來源邏輯名稱
     */
    @Override
    protected Object determineCurrentLookupKey() {
        //從共享執行緒中獲取資料來源名稱
        return DynamicDataSourceHolder.getDataSource();
    }
}

還有最後一部分在mapper介面中通過AOP來指定要使用的資料來源

import java.util.List;

@Mapper
public interface AuthMapper {

    public int save(Auth auth);

    @TargetDataSource("slave")
    public Auth getById(String Id);
}

Auth 是我自己的實體類。
@TargetDataSource(“slave”)這裡指定slave說明是走slave資料庫,將會走上邊配置的資料來源切換。沒有這個註解的都是走master資料庫。

其實DBProperties和DataSourceConfig這兩個類中的程式碼還可以繼續優化,這裡寫死了資料來源的個數,不利於擴充套件。應該動態載入才對,這個請參考我另外一篇文章Spring Boot 動態切換資料來源三——動態獲取配置檔案中的配置資訊