多數據源
使用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-multidatasource
datasource:
driver-class-name: org.hsqldb.jdbc.JDBCDriver
url: jdbc:hsqldb:mem:db1
username: sa
password:
second-datasource:
driver-class-name: org.hsqldb.jdbc.JDBCDriver
url: jdbc:hsqldb:mem:db2
username: sa
password:
這兩個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 {
@Autowired
JdbcTemplate 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/test
username: rw
password: rw_password
driver-class-name: com.mysql.jdbc.Driver
hikari:
pool-name: HikariCP
auto-commit: false
...
ro-datasource:
jdbc-url: jdbc:mysql://localhost/test
username: ro
password: ro_password
driver-class-name: com.mysql.jdbc.Driver
hikari:
pool-name: HikariCP
auto-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 {
@Override
protected Object determineCurrentLookupKey() {
return "masterDataSource";
}
}
對這個RoutingDataSource
,需要在SpringBoot中配置好並設置為主數據源:
@SpringBootApplication
public class MySpringBootApplication {
@Bean
@Primary
DataSource 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編譯參數。
另外動態數據源可以參kao https://github.com/baomidou/dynamic-datasource-spring-boot-starter