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-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拿到對應的一個真實的數據源,從而訪問指定的數據庫。它的結構看起來像這樣:
multipartDatasource

第一步:配置多數據源

首先,我們在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


免責聲明!

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



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