Spring Boot項目多數據源配置

使用場景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

相關文章
相關標籤/搜索