SpringBoot Jpa multi data source dynamic switching

Posted by jediman on Thu, 10 Feb 2022 09:15:57 +0100

In large applications, it is a common design pattern to configure the master-slave database and use read-write separation. The common implementation method is to use database middleware. This paper introduces how to realize the configuration and dynamic switching of multiple data sources by writing code. The core is to use the abstract class AbstractRoutingDataSource built in Spring, which can configure multiple data sources into a Map, and then return different data sources according to different key s.

Environment introduction

  • SpringBoot 1.5.10.RELEASE
  • MySQL 5.7

Data source configuration

First, in application Two data sources are configured in YML:

spring:
  datasource:   #Multi data source configuration
    master:
      url: jdbc:mysql://localhost:3307/testdb?useUnicode=true&characterEncoding=utf8&useSSL=false
      username: root
      password: 123456
      driver-class-name: com.mysql.jdbc.Driver
    slave:
      url: jdbc:mysql://localhost:3308/testdb?useUnicode=true&characterEncoding=utf8&useSSL=false
      username: root
      password: 123456
      driver-class-name: com.mysql.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource

  jpa:
    show-sql: true
#    hibernate:
#      naming: I don't know why this attribute can't be obtained automatically. It needs to be assigned in the code
#        physical-strategy: org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy
server:
  port: 8080
  context-path: /imooc

Initialize data source

Write the data source configuration class, initialize the data source, and encapsulate the two physical data sources into an AbstractRoutingDataSource:

@Configuration
public class DataSourceConfiguration {
    private final static String MASTER_DATASOURCE_KEY = "masterDataSource";
    private final static String SLAVE_DATASOURCE_KEY = "slaveDataSource";

    @Value("${spring.datasource.type}")
    private Class<? extends DataSource> dataSourceType;

    @Primary
    @Bean(value = MASTER_DATASOURCE_KEY)
    @Qualifier(MASTER_DATASOURCE_KEY)
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDataSource() {
        log.info("create master datasource...");
        return DataSourceBuilder.create().type(dataSourceType).build();
    }

    @Bean(value = SLAVE_DATASOURCE_KEY)
    @Qualifier(SLAVE_DATASOURCE_KEY)
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    public DataSource slaveDataSource() {
        log.info("create slave datasource...");
        return DataSourceBuilder.create().type(dataSourceType).build();
    }

    @Bean(name = "routingDataSource")
    public AbstractRoutingDataSource routingDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
            @Qualifier("slaveDataSource") DataSource slaveDataSource) {
        DynamicDataSourceRouter proxy = new DynamicDataSourceRouter();
        Map<Object, Object> targetDataSources = new HashMap<>(2);
        targetDataSources.put("masterDataSource", masterDataSource);
        targetDataSources.put("slaveDataSource", slaveDataSource);

        proxy.setDefaultTargetDataSource(masterDataSource);
        proxy.setTargetDataSources(targetDataSources);
        return proxy;
    }
}

Note that one of the data sources needs to be marked as the Primary data source with the @ Primary annotation, and the Primary data source cannot be of AbstractRoutingDataSource type, but must be of DataSource type.

Write the JpaEntityManager configuration class

After using multiple data sources, you need to manually initialize and configure the EntityManager of Jpa. You can't use the default automatic configuration. Otherwise, you can't actually create two different data sources.

@Configuration
@EnableConfigurationProperties(JpaProperties.class)
@EnableJpaRepositories(value = "com.imooc.dao.repository")
public class JpaEntityManager {

    @Autowired
    private JpaProperties jpaProperties;

    @Resource(name = "routingDataSource")
    private DataSource routingDataSource;

    //@Primary
    @Bean(name = "entityManagerFactoryBean")
    public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(EntityManagerFactoryBuilder builder) {
        // I don't understand why I can't get application here Configuration in YML
        Map<String, String> properties = jpaProperties.getProperties();
        //To set this property, implement the conversion of camelCase - > underscore
        properties.put("hibernate.physical_naming_strategy",
                "org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy");

        return builder
                .dataSource(routingDataSource)//Key: inject routingDataSource
                .properties(properties)
                .packages("com.imooc.entity")
                .persistenceUnit("myPersistenceUnit")
                .build();
    }

    @Primary
    @Bean(name = "entityManagerFactory")
    public EntityManagerFactory entityManagerFactory(EntityManagerFactoryBuilder builder) {
        return this.entityManagerFactoryBean(builder).getObject();
    }

    @Primary
    @Bean(name = "transactionManager")
    public PlatformTransactionManager transactionManager(EntityManagerFactoryBuilder builder) {
        return new JpaTransactionManager(entityManagerFactory(builder));
    }
}

Write an implementation class that dynamically saves the data source type key

Use ThreadLocal to dynamically set and save the key of data source type

public class DataSourceContextHolder {
    private static final ThreadLocal<String> holder = new ThreadLocal<>();

    public static void setDataSource(String type) {
        holder.set(type);
    }

    public static String getDataSource() {
        String lookUpKey = holder.get();
        return lookUpKey == null ? "masterDataSource" : lookUpKey;
    }

    public static void clear() {
        holder.remove();
    }
}

Implement AbstractRoutingDataSource

Write a class that inherits AbstractRoutingDataSource and overrides the determineCurrentLookupKey routing method:

public class DynamicDataSourceRouter extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSource();
    }
}

Write section to realize dynamic switching

In daily work, read and write operations are usually distinguished according to the method signature of the Service layer. The most convenient way is to use AOP to intercept:

@Slf4j
@Aspect
@Component
public class DynamicDataSourceAspect {

    @Pointcut("execution(* com.imooc.service..*.*(..))")
    private void aspect() {}

    @Around("aspect()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        String method = joinPoint.getSignature().getName();

        if (method.startsWith("find") || method.startsWith("select") || method.startsWith("query") || method
                .startsWith("search")) {
            DataSourceContextHolder.setDataSource("slaveDataSource");
            log.info("switch to slave datasource...");
        } else {
            DataSourceContextHolder.setDataSource("masterDataSource");
            log.info("switch to master datasource...");
        }

        try {
            return joinPoint.proceed();
        }finally {
            log.info("eliminate datasource router...");
            DataSourceContextHolder.clear();
        }
    }
}

summary

So far, the core code and details have been explained. Other entity classes, Repository interfaces, Service methods, test cases, etc. can be referred to.

https://github.com/linyongfu2013/springboot-multi-datasource.git
//linyongfu2013.github.io/2018/03/08/springboot-jpa-dynamic-datasource/