Spring主從數據庫的配置和動態數據源切換原理


原文:https://www.liaoxuefeng.com/article/00151054582348974482c20f7d8431ead5bc32b30354705000

 

在大型應用程序中,配置主從數據庫並使用讀寫分離是常見的設計模式。在Spring應用程序中,要實現讀寫分離,最好不要對現有代碼進行改動,而是在底層透明地支持。

Spring內置了一個AbstractRoutingDataSource,它可以把多個數據源配置成一個Map,然后,根據不同的key返回不同的數據源。因為AbstractRoutingDataSource也是一個DataSource接口,因此,應用程序可以先設置好key, 訪問數據庫的代碼就可以從AbstractRoutingDataSource拿到對應的一個真實的數據源,從而訪問指定的數據庫。它的結構看起來像這樣:

   ┌───────────────────────────┐
   │        controller         │
   │  set routing-key = "xxx"  │
   └───────────────────────────┘
                 │
                 ▼
   ┌───────────────────────────┐
   │        logic code         │
   └───────────────────────────┘
                 │
                 ▼
   ┌───────────────────────────┐
   │    routing datasource     │
   └───────────────────────────┘
                 │
       ┌─────────┴─────────┐
       │                   │
       ▼                   ▼
┌─────────────┐     ┌─────────────┐
│ read-write  │     │  read-only  │
│ datasource  │     │ datasource  │
└─────────────┘     └─────────────┘
       │                   │
       ▼                   ▼
┌─────────────┐     ┌─────────────┐
│             │     │             │
│  Master DB  │     │  Slave DB   │
│             │     │             │
└─────────────┘     └─────────────┘

第一步:配置多數據源

首先,我們在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編譯參數。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM