大家好,我是烤鸭:
今天分享springboot读写分离配置。
环境:
springboot 2.1.0.RELEASE
场景说明,目前的需求是 读数据源 * 2 + 写数据源 * 1
1. 配置文件
application.yml
server:port: 8085
spring:application:name: test-data-testdatasource:write:jdbc-url: jdbc:mysql://localhost:3306/testusername: rootpassword: test.Devdriver-class-name: com.mysql.jdbc.Drivertype: com.zaxxer.hikari.HikariDataSource connectionTimeout: 30000validationTimeout: 5000maxPoolSize: 200minIdle: 100readaw:jdbc-url: jdbc:mysql://localhost:3306/testusername: rootpassword: test!idriver-class-name: com.mysql.jdbc.Drivertype: com.zaxxer.hikari.HikariDataSource connectionTimeout: 30000validationTimeout: 5000maxPoolSize: 200minIdle: 100readdc:jdbc-url: jdbc:mysql://localhost:3306/testusername: rootpassword: test!idriver-class-name: com.mysql.jdbc.Drivertype: com.zaxxer.hikari.HikariDataSource connectionTimeout: 30000validationTimeout: 5000maxPoolSize: 200minIdle: 100
#mybatis
mybatis:###把xml文件放在com.XX.mapper.*中可能会出现找到的问题,这里把他放在resource下的mapper中mapper-mapperLocations: classpath*:mapper/**/**/*.xmltype-aliases-package: com.test.test.pojoconfiguration:map-underscore-to-camel-case: truecache-enabled: falsecall-setters-on-nulls: trueuseGeneratedKeys: true
2. 配置类
DataSourceConfig.java
默认 读数据源,如果需要增加或者减少数据源需要修改 myRoutingDataSource 方法中的参数
package com.test.test.config.db;import com.test.test.datasource.MyRoutingDataSource;
import com.test.test.datasource.enums.DBTypeEnum;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;/*** 关于数据源配置,参考SpringBoot官方文档第79章《Data Access》* 79. Data Access* 79.1 Configure a Custom DbSource* 79.2 Configure Two DataSources*/@Configuration
public class DataSourceConfig {@AutowiredEnvironment environment;@Bean@ConfigurationProperties("spring.datasource.readaw")public DataSource readDataSourceAw() {DataSource build = DataSourceBuilder.create().build();HikariDataSource hikariDataSource = buildDataSource(build,"readaw");return hikariDataSource;}@Bean@ConfigurationProperties("spring.datasource.readdc")public DataSource readDataSourceDc() {DataSource build = DataSourceBuilder.create().build();HikariDataSource hikariDataSource = buildDataSource(build,"readdc");return hikariDataSource;}@Bean@ConfigurationProperties("spring.datasource.write")public DataSource writeDataSource() {DataSource build = DataSourceBuilder.create().build();HikariDataSource hikariDataSource = buildDataSource(build,"write");return hikariDataSource;}@Beanpublic DataSource myRoutingDataSource(@Qualifier("readDataSourceAw") DataSource readDataSourceAw,@Qualifier("readDataSourceDc") DataSource readDataSourceDc,@Qualifier("writeDataSource") DataSource writeDataSource) {Map<Object, Object> targetDataSources = new HashMap<>();targetDataSources.put(DBTypeEnum.READ_AW, readDataSourceAw);targetDataSources.put(DBTypeEnum.READ_DC, readDataSourceDc);targetDataSources.put(DBTypeEnum.WRITE, writeDataSource);MyRoutingDataSource myRoutingDataSource = new MyRoutingDataSource();myRoutingDataSource.setDefaultTargetDataSource(readDataSourceAw);myRoutingDataSource.setTargetDataSources(targetDataSources);return myRoutingDataSource;}public HikariDataSource buildDataSource(DataSource dataSource,String dataSourcePrefix){HikariDataSource hikariDataSource= (HikariDataSource) dataSource;hikariDataSource.setDriverClassName(environment.getProperty("spring.datasource."+dataSourcePrefix+".driver-class-name"));hikariDataSource.setJdbcUrl(environment.getProperty("spring.datasource."+dataSourcePrefix+".jdbc-url"));hikariDataSource.setUsername(environment.getProperty("spring.datasource."+dataSourcePrefix+".username"));hikariDataSource.setPassword(environment.getProperty("spring.datasource."+dataSourcePrefix+".password"));hikariDataSource.setMinimumIdle(Integer.parseInt(environment.getProperty("spring.datasource."+dataSourcePrefix+".minIdle")));hikariDataSource.setConnectionTimeout(Long.parseLong(environment.getProperty("spring.datasource."+dataSourcePrefix+".connectionTimeout")));hikariDataSource.setValidationTimeout(Long.parseLong(environment.getProperty("spring.datasource."+dataSourcePrefix+".validationTimeout")));hikariDataSource.setMaximumPoolSize(Integer.parseInt(environment.getProperty("spring.datasource."+dataSourcePrefix+".maxPoolSize")));return hikariDataSource;}
}
MyBatisConfig.java
注意映射mapper文件路径是在这里修改的,因为重新注入了sqlSession, yml中配置的无效
package com.test.test.config.mybatis;import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;import javax.annotation.Resource;
import javax.sql.DataSource;@EnableTransactionManagement
@Configuration
public class MyBatisConfig {@Resource(name = "myRoutingDataSource")private DataSource myRoutingDataSource;@Beanpublic SqlSessionFactory sqlSessionFactory() throws Exception {SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();sqlSessionFactoryBean.setDataSource(myRoutingDataSource);sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/**/*.xml"));return sqlSessionFactoryBean.getObject();}@Beanpublic PlatformTransactionManager platformTransactionManager() {return new DataSourceTransactionManager(myRoutingDataSource);}
}
DataSourceAop.java
aop配置类,通过aop的方式限制哪个service的方法连接哪个数据源
目前是根据类上的注解来判断,可以修改为根据方法的注解来判断走哪个数据源
package com.test.test.datasource.aop;import com.test.test.datasource.annotation.DbSource;
import com.test.test.datasource.handler.DBContextHolder;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;import java.lang.reflect.Method;@Aspect
@Component
public class DataSourceAop {/*** 另一种写法:if...else... 判断哪些需要读从数据库,其余的走主数据库*/@Before("execution(* com.test.test.service.impl.*.*(..))")public void before(JoinPoint jp){MethodSignature methodSignature = (MethodSignature) jp.getSignature();Method method = methodSignature.getMethod();System.out.println("拦截到了" + jp.getSignature().getName() +"方法...");Class<?> targetClass = jp.getTarget().getClass();boolean flag = targetClass.isAnnotationPresent(DbSource.class);//包含数据源注解,数据源为注解中的类if(flag){//获取注解的valueDbSource annotation = targetClass.getAnnotation(DbSource.class);String value = annotation.value();DBContextHolder.read(value);}else {//不包含注解,查询方法默认走 默认读数据源if (StringUtils.startsWithAny(method.getName(), "get", "select", "find")) {DBContextHolder.read("");}else {DBContextHolder.write();}}}
}
DBTypeEnum.java
数据源枚举,增加和减少数据源修改即可
public enum DBTypeEnum {READ_AW, READ_DC, WRITE;}
DBContextHolder.java
数据源切换类,保持当前线程绑定哪个数据源
package com.test.test.datasource.handler;import com.test.test.datasource.enums.DBTypeEnum;
import org.apache.commons.lang3.StringUtils;import java.util.concurrent.atomic.AtomicInteger;
/*** @Author gmwang* @Description // 数据源切换类* @Date 2019/4/30 9:20* @Param* @return**/
public class DBContextHolder {private static final ThreadLocal<DBTypeEnum> contextHolder = new ThreadLocal<>();private static final AtomicInteger counter = new AtomicInteger(-1);public static void set(DBTypeEnum dbType) {contextHolder.set(dbType);}public static DBTypeEnum get() {return contextHolder.get();}public static void read(String value) {if(StringUtils.isBlank(value)){set(DBTypeEnum.READ_AW);System.out.println("切换到读"+DBTypeEnum.READ_AW.toString());}if (DBTypeEnum.READ_DC.toString().equals(value)){set(DBTypeEnum.READ_DC);System.out.println("切换到读"+DBTypeEnum.READ_DC.toString());}}public static void write() {set(DBTypeEnum.WRITE);System.out.println("切换到写"+DBTypeEnum.WRITE.toString());}
}
MyRoutingDataSource.java
多数据源的路由类
package com.test.test.datasource;import com.test.test.datasource.handler.DBContextHolder;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.lang.Nullable;
/*** @Author gmwang* @Description //多数据源的路由* @Date 2019/4/30 9:38* @Param* @return**/
public class MyRoutingDataSource extends AbstractRoutingDataSource {/*** @Author gmwang* @Description //根据Key获取数据源的信息,上层抽象函数的钩子* @Date 2019/4/30 9:39* @Param []* @return java.lang.Object**/@Nullable@Overrideprotected Object determineCurrentLookupKey() {return DBContextHolder.get();}
}
DbSource
数据源注解,加在serivice实现类上,指定 value,AOP根据注解获取指定的数据源。
package com.test.test.datasource.annotation;import java.lang.annotation.*;@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface DbSource {String value();
}
例如本例中,默认读库是 READ_AW ,如果不加注解默认,读取默认库。如果指定注解 READ_DC,就用指定的数据源。
3. 结果测试
伪代码:
在tes方法中使用查询(不同的库)后插入操作,结果如图所示。