springboot多数据源动态数据源(主从)
多數據源
使用Spring Boot時,默認情況下,配置DataSource非常容易。Spring Boot會自動為我們配置好一個DataSource。
如果在application.yml中指定了spring.datasource的相關配置,Spring Boot就會使用該配置創建一個DataSource。如果在application.yml中沒有指定任何spring.datasource的相關配置,Spring Boot會在classpath中搜索H2、hsqldb等內存數據庫的jar包,如果找到了,就會自動配置一個內存數據庫的DataSource,所以,我們只要引入jar包即可。例如,配置一個hsqldb數據源:
<dependency><groupId>org.hsqldb</groupId><artifactId>hsqldb</artifactId><scope>runtime</scope> </dependency>但是,在某些情況下,如果我們需要配置多個數據源,應該如何在Spring Boot中配置呢?
我們以JDBC為例,演示如何在Spring Boot中配置兩個DataSource。對應的,我們會創建兩個JdbcTemplate的Bean,分別使用這兩個數據源。
首先,我們必須在application.yml中聲明兩個數據源的配置,一個使用spring.datasource,另一個使用spring.second-datasource:
spring:application:name: data-multidatasourcedatasource:driver-class-name: org.hsqldb.jdbc.JDBCDriverurl: jdbc:hsqldb:mem:db1username: sapassword:second-datasource:driver-class-name: org.hsqldb.jdbc.JDBCDriverurl: jdbc:hsqldb:mem:db2username: sapassword:這兩個DataSource都使用hsqldb,但是數據庫是不同的。此外,在使用多數據源的時候,所有必要配置都不能省略。
其次,我們需要自己創建兩個DataSource的Bean,其中一個標記為@Primary,另一個命名為secondDatasource:
@Configuration public class SomeConfiguration {@Bean@Primary@ConfigurationProperties(prefix = "spring.datasource")public DataSource primaryDataSource() {return DataSourceBuilder.create().build();}@Bean(name = "secondDatasource")@ConfigurationProperties(prefix = "spring.second-datasource")public DataSource secondDataSource() {return DataSourceBuilder.create().build();} }對于每一個DataSource,我們都必須通過@ConfigurationProperties(prefix = "xxx")指定配置項的前綴。
緊接著,我們創建兩個``JdbcTemplate的Bean,其中一個標記為@Primary,另一個命名為secondJdbcTemplate,分別使用對應的DataSource:
@Bean @Primary public JdbcTemplate primaryJdbcTemplate(DataSource dataSource) {return new JdbcTemplate(dataSource); }@Bean(name = "secondJdbcTemplate") public JdbcTemplate secondJdbcTemplate(@Qualifier("secondDatasource") DataSource dataSource) {return new JdbcTemplate(dataSource); }注意到secondJdbcTemplate在創建時,傳入的DataSource必須用@Qualifier("secondDatasource")聲明,這樣,才能使用第二個DataSource。
現在,我們就創建了兩個JdbcTemplate的Bean。在需要使用第一個JdbcTemplate的地方,我們直接注入:
@Component public class SomeService {@AutowiredJdbcTemplate jdbcTemplate; }在需要使用第二個JdbcTemplate的地方,我們注入時需要用@Qualifier("secondJdbcTemplate")標識:
@Component public class AnotherService {@Autowired@Qualifier("secondJdbcTemplate")JdbcTemplate secondJdbcTemplate; }這樣,我們就可以針對不同的數據源,用不同的JdbcTemplate進行操作。
注意事項
當存在多個相同類型的Bean,例如,多個DataSource,多個JdbcTemplate時,強烈建議總是使用@Primary把其中某一個Bean標識為“主要的”,使用@Autowired注入時會首先使用被標記為@Primary的Bean。
相同類型的其他Bean,每一個都需要用@Bean(name="xxx")標識名字,并且,在使用@Autowired注入時配合@Qualifier("xxx")指定注入的Bean的名字。
完整的示例工程源碼請參考:
https://github.com/michaelliao/springcloud/tree/master/data-multidatasource
動態數據源
在大型應用程序中,配置主從數據庫并使用讀寫分離是常見的設計模式。在Spring應用程序中,要實現讀寫分離,最好不要對現有代碼進行改動,而是在底層透明地支持。
Spring內置了一個AbstractRoutingDataSource,它可以把多個數據源配置成一個Map,然后,根據不同的key返回不同的數據源。因為AbstractRoutingDataSource也是一個DataSource接口,因此,應用程序可以先設置好key, 訪問數據庫的代碼就可以從AbstractRoutingDataSource拿到對應的一個真實的數據源,從而訪問指定的數據庫。它的結構看起來像這樣:
第一步:配置多數據源
首先,我們在SpringBoot中配置兩個數據源,其中第二個數據源是ro-datasource:
spring:datasource:jdbc-url: jdbc:mysql://localhost/testusername: rwpassword: rw_passworddriver-class-name: com.mysql.jdbc.Driverhikari:pool-name: HikariCPauto-commit: false...ro-datasource:jdbc-url: jdbc:mysql://localhost/testusername: ropassword: ro_passworddriver-class-name: com.mysql.jdbc.Driverhikari:pool-name: HikariCPauto-commit: false...在開發環境下,沒有必要配置主從數據庫。只需要給數據庫設置兩個用戶,一個rw具有讀寫權限,一個ro只有SELECT權限,這樣就模擬了生產環境下對主從數據庫的讀寫分離。
在SpringBoot的配置代碼中,我們初始化兩個數據源:
@SpringBootApplication public class MySpringBootApplication {/*** Master data source.*/@Bean("masterDataSource")@ConfigurationProperties(prefix = "spring.datasource")DataSource masterDataSource() {logger.info("create master datasource...");return DataSourceBuilder.create().build();}/*** Slave (read only) data source.*/@Bean("slaveDataSource")@ConfigurationProperties(prefix = "spring.ro-datasource")DataSource slaveDataSource() {logger.info("create slave datasource...");return DataSourceBuilder.create().build();}... }第二步:編寫RoutingDataSource
然后,我們用Spring內置的RoutingDataSource,把兩個真實的數據源代理為一個動態數據源:
public class RoutingDataSource extends AbstractRoutingDataSource {@Overrideprotected Object determineCurrentLookupKey() {return "masterDataSource";} }對這個RoutingDataSource,需要在SpringBoot中配置好并設置為主數據源:
@SpringBootApplication public class MySpringBootApplication {@Bean@PrimaryDataSource primaryDataSource(@Autowired @Qualifier("masterDataSource") DataSource masterDataSource,@Autowired @Qualifier("slaveDataSource") DataSource slaveDataSource) {logger.info("create routing datasource...");Map<Object, Object> map = new HashMap<>();map.put("masterDataSource", masterDataSource);map.put("slaveDataSource", slaveDataSource);RoutingDataSource routing = new RoutingDataSource();routing.setTargetDataSources(map);routing.setDefaultTargetDataSource(masterDataSource);return routing;}... }現在,RoutingDataSource配置好了,但是,路由的選擇是寫死的,即永遠返回"masterDataSource",
- 現在問題來了:如何存儲動態選擇的key以及在哪設置key?
在Servlet的線程模型中,使用ThreadLocal存儲key最合適,因此,我們編寫一個RoutingDataSourceContext,來設置并動態存儲key:
public class RoutingDataSourceContext implements AutoCloseable {// holds data source key in thread local:static final ThreadLocal<String> threadLocalDataSourceKey = new ThreadLocal<>();public static String getDataSourceRoutingKey() {String key = threadLocalDataSourceKey.get();return key == null ? "masterDataSource" : key;}public RoutingDataSourceContext(String key) {threadLocalDataSourceKey.set(key);}public void close() {threadLocalDataSourceKey.remove();} }然后,修改RoutingDataSource,獲取key的代碼如下:
public class RoutingDataSource extends AbstractRoutingDataSource {protected Object determineCurrentLookupKey() {return RoutingDataSourceContext.getDataSourceRoutingKey();} }這樣,在某個地方,例如一個Controller的方法內部,就可以動態設置DataSource的Key:
@Controller public class MyController {@Get("/")public String index() {String key = "slaveDataSource";try (RoutingDataSourceContext ctx = new RoutingDataSourceContext(key)) {// TODO:return "html... www.liaoxuefeng.com";}} }到此為止,我們已經成功實現了數據庫的動態路由訪問。
這個方法是可行的,但是,需要讀從數據庫的地方,就需要加上一大段try (RoutingDataSourceContext ctx = ...) {}代碼,使用起來十分不便。有沒有方法可以簡化呢?
有!
我們仔細想想,Spring提供的聲明式事務管理,就只需要一個@Transactional()注解,放在某個Java方法上,這個方法就自動具有了事務。
我們也可以編寫一個類似的@RoutingWith("slaveDataSource")注解,放到某個Controller的方法上,這個方法內部就自動選擇了對應的數據源。代碼看起來應該像這樣:
@Controller public class MyController {@Get("/")@RoutingWith("slaveDataSource")public String index() {return "html... www.liaoxuefeng.com";} }這樣,完全不修改應用程序的邏輯,只在必要的地方加上注解,自動實現動態數據源切換,這個方法是最簡單的。
想要在應用程序中少寫代碼,我們就得多做一點底層工作:必須使用類似Spring實現聲明式事務的機制,即用AOP實現動態數據源切換。
實現這個功能也非常簡單,編寫一個RoutingAspect,利用AspectJ實現一個Around攔截:
@Aspect @Component public class RoutingAspect {@Around("@annotation(routingWith)")public Object routingWithDataSource(ProceedingJoinPoint joinPoint, RoutingWith routingWith) throws Throwable {String key = routingWith.value();try (RoutingDataSourceContext ctx = new RoutingDataSourceContext(key)) {return joinPoint.proceed();}} }注意方法的第二個參數RoutingWith是Spring傳入的注解實例,我們根據注解的value()獲取配置的key。編譯前需要添加一個Maven依賴:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId> </dependency>到此為止,我們就實現了用注解動態選擇數據源的功能。最后一步重構是用字符串常量替換散落在各處的"masterDataSource"和"slaveDataSource"。
使用限制
受Servlet線程模型的局限,動態數據源不能在一個請求內設定后再修改,也就是@RoutingWith不能嵌套。此外,@RoutingWith和@Transactional混用時,要設定AOP的優先級。
本文代碼需要SpringBoot支持,JDK 1.8編譯并打開-parameters編譯參數。
總結
以上是生活随笔為你收集整理的springboot多数据源动态数据源(主从)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 使用jQuery实现图片懒加载原理
- 下一篇: 使用Nginx过滤网络爬虫