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 動態切換資料來源三——動態獲取配置檔案中的配置資訊。