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/