Spring Boot realizes read-write separation. No one else can't?

Posted by mustang on Mon, 03 Jan 2022 15:06:15 +0100

Step 1: configure multiple data sources

I won't introduce the basics of Spring Boot. I recommend this practical tutorial: github.com/javastacks/...

First, we configure two data sources in SpringBoot. The second data source 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 code

In the development environment, it is not necessary to configure the master-slave database. Only two users need to be set for the database. One rw has read-write permission and the other ro has SELECT permission. This simulates the read-write separation of the master-slave database 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 code

Step 2: write RoutingDataSource

Then, we use Spring's built-in RoutingDataSource to proxy two real data sources into a dynamic data source:

public class RoutingDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return "masterDataSource";
    }
}
Copy code

For this RoutingDataSource, you need to configure it in SpringBoot and set it as the main 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 code

Now, the RoutingDataSource is configured, but the route selection is dead, that is, it always returns "masterDataSource",

Now the question comes: how to store the dynamically selected key and where to set the key?

In the Servlet thread model, ThreadLocal is the most appropriate way to store keys. Therefore, we write a RoutingDataSourceContext to set and dynamically store keys:

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 code

Then, modify the RoutingDataSource to obtain the key code as follows:

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

In this way, the Key of the DataSource can be dynamically set somewhere, such as within a Controller method:

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

So far, we have successfully realized the dynamic routing access of the database.

This method is feasible, but a large section of try (RoutingDataSourceContext ctx =...) needs to be added where the slave database needs to be read {} code, very inconvenient to use. Is there any way to simplify it?

have

Let's think about it carefully. The declarative transaction management provided by Spring only needs an @ Transactional() annotation on a Java method, which automatically has transactions.

We can also write a similar @ RoutingWith("slaveDataSource") annotation and put it on a Controller method, which automatically selects the corresponding data source. The code should look like this:

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

In this way, the logic of the application program is not modified at all, and only annotations are added where necessary to automatically realize dynamic data source switching. This method is the simplest.

If we want to write less code in the application, we have to do more underlying work: we must use a mechanism similar to Spring to implement declarative transactions, that is, AOP to realize dynamic data source switching.

It is also very simple to implement this function. Write a RoutingAspect and use AspectJ to implement an Around interception:

@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 code

Note that the second parameter RoutingWith of the method is the annotation instance passed in by Spring. We get the configured key according to the annotation value(). You need to add a Maven dependency before compiling:

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

So far, we have realized the function of dynamically selecting data sources with annotations. The last step of refactoring is to replace the scattered "masterDataSource" and "slaveDataSource" with string constants.

Use restrictions

Due to the limitation of Servlet thread model, dynamic data sources cannot be set in a request and then modified, that is, @ RoutingWith cannot be nested. In addition, when @ RoutingWith and @ Transactional are mixed, AOP priority should be set.

The code in this article needs spring boot support. JDK 1.8 compiles and opens the - parameters compilation parameter.

Original address: Home - Liao Xuefeng's official website

If you think this article is helpful to you, please praise it, pay attention and support it

 

Topics: Java Spring Spring Boot Programmer