Configuring master/slave databases and using read/write separation is a common design pattern in large applications. To achieve read/write separation in Spring applications, it is best not to make changes to existing code, but to support it transparently underneath.

Spring built-in a AbstractRoutingDataSource, it can put the configuration of multiple data sources into a Map, and then, according to the different key return different data sources. Because AbstractRoutingDataSource is a DataSource interface, therefore, the application to set the key, database access code can be from the AbstractRoutingDataSource corresponds to a real data source, To access the specified database. Its structure looks something like this:

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

Step 1: Configure multiple data sources

First, we configure two data sources in SpringBoot, the second of which is 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
      ...
Copy the code

In a development environment, it is not necessary to configure the master and slave databases. You only need to set two users for the database, one rW has read and write permission, and the other RO has SELECT permission. In this way, the separation between the primary and secondary databases is simulated in the production environment.

In the SpringBoot configuration code, we initialize two data sources:

@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(); }... }Copy the code

Step 2: Write the RoutingDataSource

We then use Spring’s built-in RoutingDataSource to proxy two real data sources as one dynamic data source:

public class RoutingDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return "masterDataSource"; }}Copy the code

For the RoutingDataSource, you need to configure it in SpringBoot and set it to the primary data source:

@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;
    }
    ...
}
Copy the code

Now, the RoutingDataSource is configured, but the route selection is written dead, that is, the “masterDataSource” is always returned,

Now the question is: how to store dynamically selected keys and where to set them?

In the Servlet threading model, it is best to store keys using ThreadLocal, so we write a RoutingDataSourceContext to set and store keys dynamically:

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(); }}Copy the code

Then modify the RoutingDataSource to get the key as follows:

public class RoutingDataSource extends AbstractRoutingDataSource {
    protected Object determineCurrentLookupKey() {
        return RoutingDataSourceContext.getDataSourceRoutingKey();
    }
}
Copy the code

This way, somewhere, such as inside a Controller’s method, the DataSource Key can be set dynamically:

@Controller public class MyController { @Get("/") public String index() { String key = "slaveDataSource"; try (RoutingDataSourceContext ctx = new RoutingDataSourceContext(key)) { // TODO: return "html... www.liaoxuefeng.com"; }}}Copy the code

So far, we have successfully implemented dynamic routing access to the database.

This method works, but to read from the database, you need to add a large try (RoutingDataSourceContext CTX =…). {} code, very inconvenient to use. Is there a way to simplify this?

There are!

If you think about it, Spring provides declarative transaction management that simply requires a @Transactional() annotation ona Java method that automatically has a transaction.

We could also write a similar @routingWith (“slaveDataSource”) annotation on a Controller’s method that automatically selects the corresponding data source internally. The code should look like this:

@Controller public class MyController { @Get("/") @RoutingWith("slaveDataSource") public String index() { return "html... www.liaoxuefeng.com"; }}Copy the code

In this way, it is easiest to automatically implement dynamic data source switching without changing the application logic at all, only annotating where necessary.

To write less code in your application, you have to do a little more low-level work: you have to use a mechanism similar to Spring’s for declarative transactions, which uses AOP to implement dynamic data source switching.

Implementing this functionality is as simple as writing a RoutingAspect that uses AspectJ to implement an Around intercept:

@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(); }}}Copy the code

Note that the method’s second argument, RoutingWith, is an instance of an annotation passed in by Spring, and we get the configured key from the annotation’s value(). A Maven dependency needs to be added before compilation:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
Copy the code

At this point, we have implemented the ability to dynamically select data sources with annotations. The final refactoring is to replace the scattered “masterDataSource” and “slaveDataSource” with string constants.

Use restrictions

Due to the limitations of the Servlet threading model, dynamic data sources cannot be set and then modified within a request, i.e. @routingWith cannot be nested. Additionally, when @RoutingWith and @Transactional are used together, AOP priority is set.

This code requires SpringBoot support, JDK 1.8 compiles and opens the -parameters compiler parameter.