使用場景java
多數據源的使用在分庫的狀況下並不稀奇,而平時的項目需求就不多見了。之前我也沒去琢磨過,只是前陣子項目新的需求恰好須要,並且還不是同一種數據庫。mysql
個人奇葩新需求須要實現三個數據庫動態切換,一個是你們都知道的mysql,一個是亞馬遜的Redshift,還有一個也是亞馬遜的服務Athena。爲了都能使用mybatis,須要實現自動識別數據庫從而切換數據源。有一個特殊,因爲Athena的jdbc奇葩,mybatis並不兼容,只能使用古版的jdbc直操方式,但也不影響使用動態數據源。算法
Spring Boot項目中多數據源的配置spring
爲了簡單,我會縮減一些你們不用看也明白的代碼。我將以實現三個不一樣數據庫的數據源動態切換爲例子,更貼近真實需求,講述如何在spring boot項目中配置多數據源。但願各位看官可以耐心看完。sql
我選擇拋棄spring boot提供的jdbc配置,本身定義多數據源的jdbc鏈接參數配置。而且我也加入了鏈接池的支持,多個數據源可使用同一份鏈接池配置,前提是多個數據源都使用同一種鏈接池,如阿里雲的druid鏈接池。網上不少都沒有介紹到多數據源鏈接池的配置,不要緊,看我這篇就夠了。數據庫
一、配置jdbc鏈接信息編程
Mysql數據源的配置
設計模式
mysql:
driverClassName: com.mysql.jdbc.Driver
jdbcUrl: jdbc:mysql://localhost:3306/xxxx?characterEncoding=utf8&useSSL=false
username: root
password:
Redshift數據源配置mybatis
redshift:
driverClassName: com.amazon.redshift.jdbc42.Driver
jdbcUrl: jdbc:redshift://xxx.xxxx.com:5439/xxxx
username: xxxxx
password: xxxxx
AWS Athena數據源配置多線程
athena:
username: xxxx
password: xxxx
aws-region: xxxx
s3-outputlocation: s3://xxxxx/
url: jdbc:awsathena://AwsRegion=%s;UID=%s;PWD=%s;S3OutputLocation=%s;
driver: com.simba.athena.jdbc.Driver
二、配置鏈接池信息
在resources下建立一個鏈接池配置文件,我選擇使用properties文件,由於我要實現本身讀取配置信息。
druid.properties 數據庫鏈接池配置信息。由於mysql和redshift均可以使用druid鏈接池,而Athena是個奇葩,就無論了,由於也不多用到,因此不給Athena配置鏈接池。
dataSource.initialSize=20
dataSource.minIdle=1
dataSource.maxIdle=20
dataSource.maxActive=100
dataSource.maxWait=60000
我還爲此寫了一個properties配置讀取工具,實現自動讀取指定配置信息,並映射爲java對象返回。這裏涉及到兩個類,一個是PropertiesAnnotation,另外一個是PropertiesUtils。前者是一個註解,後者實現讀取配置信息並使用反射建立對象爲對象的字段賦值,詳細介紹請往下看。
先來建立一個用於接收數據庫鏈接池配置信息的java類。字段名須要與配置文件中的字段名相同。並使用@PropertiesAnnotation註解聲明須要從哪一個文件讀取,配置的前綴是什麼。你可能不理解前綴是什麼,好比前面的數據庫鏈接池配置:dataSource.initialSize,那麼前綴就是dataSource,若是沒有前綴則寫""。
@PropertiesAnnotation(filePath = "database/druid.properties", prefix = "dataSource")
@NoArgsConstructor
@Data
public class DruidConfig {
private Integer initialSize;
private Integer minIdle;
private Integer maxIdle;
private Integer maxActive;
private Integer maxWait;
}
建立PropertiesAnnotation註解,本應該是先建立該註解的,但我爲了習慣你們的理解順序(按瀏覽順序理解)。PropertiesAnnotation聲明爲運行時使用在類上的註解,只有兩個屬性,一個是文件基於classpath的路徑,另外一個就是配置信息的前綴,已經在前面說過了。
/**
* @author wujiuye
* @version 1.0 on 2019/4/24 {描述:}
*/
@Target({ElementType.TYPE})
@Retention(RUNTIME)
@Documented
public @interface PropertiesAnnotation {
/**
* 文件路徑,基於classpath
*/
String filePath();
/**
* 屬性名前綴
*/
String prefix();
}
PropertiesUtils實現的功能就是自動讀取解析properties配置文件,只須要傳遞你用來接收配置信息的java類便可。getPropertiesConfig方法會讀取java類(Class)上的@PropertiesAnnotation註解,並從註解中獲取到文件的路徑,以及配置的前綴信息,如database,而後讀取配置文件,從配置文件中找到前綴爲database的配置。根據反射生成一個java對象,並將配置信息賦值給java對象中對應的字段。一時看不明白不要緊,能夠跳過日後看。
/**
* @author wujiuye
* @version 1.0 on 2019/4/24 {描述:}
*/
public class PropertiesUtils {
/**
* 獲取配置文件內容
*
* @return
*/
public static <T> T getPropertiesConfig(Class<T> configClass) throws Exception {
ClassLoader loader = configClass.getClassLoader();
T obj = configClass.newInstance();
Properties properties = new Properties();
//獲取註解信息
PropertiesAnnotation propertiesAnnotation = configClass.getAnnotation(PropertiesAnnotation.class);
if (propertiesAnnotation == null) {
throw new Exception("not found @PropertiesAnnotation annotation!!!");
}
if (StringUtil.isEmpty(propertiesAnnotation.filePath())) {
throw new Exception("file path is null!!!");
}
String prefix = propertiesAnnotation.prefix();
if (StringUtil.isEmpty(prefix)) {
prefix = "";
} else {
prefix += '.';
}
try (InputStream in = loader.getResourceAsStream(propertiesAnnotation.filePath())) {
properties.load(new InputStreamReader(in, "utf-8"));
Field[] fields = configClass.getDeclaredFields();
if (fields == null || fields.length == 0) {
return obj;
}
for (Field field : fields) {
field.setAccessible(true);
String value = properties.getProperty(prefix + field.getName());
if (field.getType() == Integer.class || field.getType() == int.class) {
field.set(obj, Integer.valueOf(value));
} else if (field.getType() == Long.class || field.getType() == long.class) {
field.set(obj, Long.valueOf(value));
} else if (field.getType() == Boolean.class || field.getType() == boolean.class) {
field.set(obj, Boolean.valueOf(value));
} else {
field.set(obj, value);
}
}
} catch (Exception e) {
throw e;
}
return obj;
}
}
三、先配置這三個數據源
如何獲取數據源的配置信息不用說了吧,最簡單的可使用@Value。如mysql數據源配置信息,其它的本身花幾秒鐘腦補。
@Value("${mysql.driverClassName}")
private String driverClassName;
@Value("${mysql.jdbcUrl}")
private String jdbcUrl;
@Value("${mysql.username}")
private String username;
@Value("${mysql.password}")
private String password;
建立一個數據源的配置類DataSourceConfiguration。
a、配置mysql數據源
//mysql數據源
@Bean(name = "mysql-database")
public DataSource mysqlDatabase() {
DruidDataSource druidDataSource = new DruidDataSource();
//配置jdbc
druidDataSource.setDriverClassName(driverClassName);
druidDataSource.setUrl(jdbcUrl);
druidDataSource.setUsername(username);
druidDataSource.setPassword(password);
//配置鏈接池信息
PropertiesUtils.DruidConfig druidConfig = PropertiesUtils.getPropertiesConfig(PropertiesUtils.DruidConfig.class);
druidDataSource.setMinIdle(druidConfig.getMinIdle());
druidDataSource.setMaxWait(druidConfig.getMaxWait());
druidDataSource.setMaxActive(druidConfig.getMaxActive());
druidDataSource.setInitialSize(druidConfig.getInitialSize());
.....
return druidDataSource;
}
Redshift數據源的配置同mysql數據源的配置,這裏要說的是另外一個奇葩數據源Athena的配置。
//athena數據源,比較特殊
@Bean(name = "athena-database")
public DataSource athenaDatabase() {
//加載athena的驅動類
try {
Class.forName(ATHENA_DRIVER);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
com.simba.athena.jdbc.DataSource dataSource = new com.simba.athena.jdbc.DataSource();
String url = String.format(ATHENA_URL, ATHENA_REGION, ATHENA_USERNAME, ATHENA_PASSWORD, ATHENA_S3_OUTPUTLOCATION);
dataSource.setURL(url);
return dataSource;
}
四、配置動態數據源
這裏須要使用spring jdbc框架提供的一個類AbstractRoutingDataSource,這是一個讓咱們輕鬆實現多數據切換使用的抽象類,須要咱們繼承該類,實現具體的切換邏輯。AbstractRoutingDataSource不須要引入多餘的依賴,這是spring框架自己提供的,若是你的項目使用不了這個類,那就應該是spring boot版本的問題了。
/**
* @author wujiuye
* @version 1.0 on 2019/4/17 {描述:
* 使用Spring的AbstractRoutingDataSource實現多數據源切換
* }
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* 動態設置數據源
* 配置多數據源時map的key
* @return
*/
@Override
protected Object determineCurrentLookupKey() {
System.out.println("數據源爲" + DataSourceContextHolder.getDataSource());
return DataSourceContextHolder.getDataSource();
}
}
determineCurrentLookupKey方法就是用來實現具體的切換邏輯的,你會看到,這裏只是用了DataSourceContextHolder.getDataSource()方法返回當前須要使用的數據源。由於一次數據庫訪問操做是在一個線程內完成的,因此我使用ThreadLocal來存儲當前jdbc線程應該使用哪一個數據源,而具體使用哪一個數據須要在mapper方法執行以前使用AOP設置,後面具體講。
determineCurrentLookupKey方法返回的是數據源的key,由於多個數據源是使用一個map存儲的,你也能夠理解爲spring管理bean的name,但最好不要這麼理解,稍後看動態數據源dynamicDataSource配置的時候就能明白了。
/**
* 動態數據源: 經過AOP在不一樣數據源之間動態切換
*
* @return
*/
@Primary
@Bean(name = "dynamicDataSource")
public DataSource dynamicDataSource() {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
// 默認數據源,當沒有使用@DataSource註解時使用,
// 而使用了@DataSource註解若是沒有設置beanName也要Aop本身配置使用默認的bean
dynamicDataSource.setDefaultTargetDataSource(mysqlDatabase());
// 配置多數據源
// key -> bean
Map<Object, Object> dsMap = new HashMap();
dsMap.put("mysqlDatabase", mysqlDatabase());
dsMap.put("athenaDatabase", athenaDatabase());
dynamicDataSource.setTargetDataSources(dsMap);
return dynamicDataSource;
}
/**
* 配置@Transactional事物註解
* 使用動態數據源
*
* @return
*/
@Bean
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dynamicDataSource());
}
注意,事務的管理也須要配置爲使用動態數據源,不然@Transactional的使用會有意想不到的意外。但其實你配置了也有意外,就是使用事務以後動態數據源就不生效了。
/**
* @author wujiuye
* @version 1.0 on 2019/4/17 {描述:}
*/
public class DataSourceContextHolder {
private final static String DEFAULT_DATASOURCE = "mysqlDatabase";
public static String getDefaultDataSourceBeanName() {
return DataSourceContextHolder.DEFAULT_DATASOURCE;
}
/**
* 保存的是多數據源Map的key
*/
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
// 設置數據源Key
public static void setDataSource(String dataSourceKey) {
//System.out.println("切換到{" + dataSourceKey + "}數據源");
contextHolder.set(dataSourceKey);
}
// 獲取當前線程應該使用的數據源的key
public static String getDataSource() {
return (contextHolder.get());
}
// 清除當前線程使用的數據源的key
public static void clearDataSource() {
contextHolder.remove();
}
}
五、實現數據源的動態切換
其實,到第四步多數據源就算配置完成了,可是最關鍵的動態根據業務切換數據源尚未實現。若是你認爲這樣就完了,那麼就只會永遠使用默認的數據源。
如何使用AOP實現動態數據源的切換,固然是實現一個切面,定義一個切點,而切點就是全部mapper(mybatis)接口中的方法。使用基於註解的切點會很容易實現這一需求。先建立一個註解@DataSource,只須要一個屬性便可,用來標明該方法使用哪一個數據源。
/**
* @author wujiuye
* @version 1.0 on 2019/4/17 {描述:
* 用於切換數據源
* }
*/
@Target({ElementType.METHOD})
@Retention(RUNTIME)
@Documented
public @interface DataSource {
//數據源的Key
String value();
}
接着就是實現AOP類,咱們並不須要干涉具體的數據庫執行增刪改查的操做,因此不要使用環繞加強,使用前置加強便可。同時由於使用ThreadLocal存儲切換的數據源,應該在方法執行完成以後清除設置,避免污染其它未使用@database註解的mapper方法,應爲線程重用問題。AOP具體的實現代碼以下。
@Aspect
@Component
public class DynamicDataSourceAop {
/**
* 定義數據源切點
*/
@Pointcut("@annotation(com.hippo.cayman.annotation.DataSource)")
public void dataSourcePointcut() {
}
/**
* 根據註解動態設置數據源,沒有註解的使用默認數據源
*
* @param point
*/
@Before("dataSourcePointcut()&&@annotation(dataSource)")
public void beforeSettingDataSource(JoinPoint point, DataSource dataSource) {
String dataSourceKey = DataSourceContextHolder.getDefaultDataSourceKey();
try {
if (dataSource.value() != null) {
dataSourceKey = dataSource.value();
}
} catch (Exception e) {
e.printStackTrace();
}
// 切換數據源
DataSourceContextHolder.setDataSource(dataSourceKey);
}
/**
* 方法執行完成以後須要移除設置的數據源(ThreadLocal保存的要清除)
*/
@After("dataSourcePointcut()")
public void afterRemoveSetting() {
DataSourceContextHolder.clearDataSource();
}
}
注意一點,方法執行異常也須要清除數據源的設置。
/**
* 執行異常也須要清除
*/
@AfterThrowing("dataSourcePointcut()")
public void afterExceptionSetting() {
DataSourceContextHolder.clearDataSource();
}
六、使用
使用很簡單,在mapper類的方法上使用註解聲明須要使用的數據源便可。
public interface UserMapper {
@DataSource("athenaDatabase")
@Select("select * from sys_account where username=#{username} limit 1")
User selectByUsername(String username);
}
當前存在的缺點
不支持使用@Transactional事務,由於@Transactional是加在Service層的。
一、使用事務時,數據源切換失敗。
使用了@Transactional,則每次都會從TransactionUtils的ThreadLocal中去拿數據源,若是爲空,就建立新的鏈接,若是不爲空的話直接使用,而ThreadLocal是線程的變量,由於ThreadLocal不爲空,因此這就會致使數據源切換失敗。
二、使用事務,同時也使用線程池時須要注意
若是你使用了數據庫鏈接線程池,那麼前面第一點的問題你就很難解決了。關於第一點網頁有不少解決方法,可是我目前還不須要用到,若是我用到我會去研究源碼再給你們分享解決方案,在此先埋下一個坑,由於確實沒有時間折騰這東西。
總結
多數據源的配置就介紹到這裏,若是想玩出花樣,還須要本身去琢磨,不本身思考是感覺不到那種成功的喜悅的。有時候也須要有種動力逼迫本身去學習,好比換工做帶來的競爭壓力,再好比主動挑戰複雜的業務需求。也能夠是主動去優化項目代碼。個人代碼優勢是能合理的應用設計模式,將對外提供的功能儘量封裝,讓使用更簡單,讓代碼閱讀更清晰,同時,也會考慮到擴展性、兼容性,能使用算法的地方儘量的使用,多線程編程須要考慮性能,jdk新特性要用好,其實這些我以前也發過一篇文章專門聊代碼優化的。
https://mp.weixin.qq.com/s/_OK3AgYoyoqu7QHY8QWLjw