环境:springboot2.2.6RELEASE
实现目标:一写多读,读可以任意配置多个,默认都是从写库中进行操作,只有符合条件的方法(指定的目标方法或者标有指定注解的方法才会从读库中操作)。独立打成一个jar包放入本地仓库。
实现原理:通过aop。
1.pom.xml配置文件
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter</artifactId>
- </dependency>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-jpa</artifactId>
- </dependency>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-configuration-processor</artifactId>
- <optional>true</optional>
- </dependency>
2.application.yml配置文件
- pack:
- datasource:
- pointcut: execution(public * net.greatsoft.service.base.*.*(..)) || execution(public * net.greatsoft.service.xxx.*.*(..))
- master:
- driverClassName: oracle.jdbc.driver.OracleDriver
- jdbcUrl: jdbc:oracle:thin:@10.100.102.113:1521/orcl
- username: test
- password: test
- minimumIdle: 10
- maximumPoolSize: 200
- autoCommit: true
- idleTimeout: 30000
- poolName: MbookHikariCP
- maxLifetime: 1800000
- connectionTimeout: 30000
- connectionTestQuery: SELECT 1 FROM DUAL
- slaves:
- - driverClassName: oracle.jdbc.driver.OracleDriver
- jdbcUrl: jdbc:oracle:thin:@10.100.102.113:1521/orcl
- username: dc
- password: dc
- minimumIdle: 10
- maximumPoolSize: 200
- autoCommit: true
- idleTimeout: 30000
- poolName: MbookHikariCP
- maxLifetime: 1800000
- connectionTimeout: 30000
- connectionTestQuery: SELECT 1 FROM DUAL
- - driverClassName: oracle.jdbc.driver.OracleDriver
- jdbcUrl: jdbc:oracle:thin:@10.100.102.113:1521/orcl
- username: empi
- password: empi
- minimumIdle: 10
- maximumPoolSize: 200
- autoCommit: true
- idleTimeout: 30000
- poolName: MbookHikariCP
- maxLifetime: 1800000
- connectionTimeout: 30000
- connectionTestQuery: SELECT 1 FROM DUAL
pointcut:定义切点,那些方法是需要拦截(从读库中操作)。
master:写库配置。
slaves:读库配置(List集合)。
3.属性配置类
- @Component
- @ConfigurationProperties(prefix = "pack.datasource")
- public class RWDataSourceProperties {
- private String pointcut ;
- private HikariConfig master ;
- private List<HikariConfig> slaves = new ArrayList<>();
- }
4.读写配置类
- public class RWConfig {
- private static Logger logger = LoggerFactory.getLogger(RWConfig.class) ;
- @Bean
- public HikariDataSource masterDataSource(RWDataSourceProperties rwDataSourceProperties) {
- return new HikariDataSource(rwDataSourceProperties.getMaster()) ;
- }
- @Bean
- public List<HikariDataSource> slaveDataSources(RWDataSourceProperties rwDataSourceProperties) {
- List<HikariDataSource> lists = new ArrayList<>() ;
- for(HikariConfig config : rwDataSourceProperties.getSlaves()) {
- lists.add(new HikariDataSource(config)) ;
- }
- return lists ;
- }
- @Bean
- @Primary
- @DependsOn({"masterDataSource", "slaveDataSources"})
- public AbstractRoutingDataSource routingDataSource(@Qualifier("masterDataSource")DataSource masterDataSource,
- @Qualifier("slaveDataSources")List<HikariDataSource> slaveDataSources) {
- BaseRoutingDataSource ds = new BaseRoutingDataSource() ;
- Map<Object, Object> targetDataSources = new HashMap<>(2) ;
- targetDataSources.put("master", masterDataSource) ;
- for (int i = 0; i < slaveDataSources.size(); i++) {
- targetDataSources.put("slave-" + i, slaveDataSources.get(i)) ;
- }
- ds.setDefaultTargetDataSource(masterDataSource) ;
- ds.setTargetDataSources(targetDataSources) ;
- return ds ;
- }
- }
5.数据源路由
- public class BaseRoutingDataSource extends AbstractRoutingDataSource {
- @Resource
- private DataSourceHolder holder;
- @Override
- protected Object determineCurrentLookupKey() {
- return holder.get() ;
- }
- }
- public class DataSourceHolder {
- private ThreadLocal<Integer> context = new ThreadLocal<Integer>() {
- @Override
- protected Integer initialValue() {
- return 0 ;
- }
- };
- @Resource
- private BaseSlaveLoad slaveLoad ;
- public String get() {
- Integer type = context.get() ;
- return type == null || type == 0 ? "master" : "slave-" + slaveLoad.load() ;
- }
- public void set(Integer type) {
- context.set(type) ;
- }
- }
通过aop动态设置context的内容值,0为从写库中操作,其它的都在读库中操作。
BaseSlaveLoad类为到底从那个读库中选取的一个算法类,默认实现使用的是轮询算法。
- public interface BaseSlaveLoad {
- int load() ;
- }
- public abstract class AbstractSlaveLoad implements BaseSlaveLoad {
- @Resource
- protected List<HikariDataSource> slaveDataSources ;
- }
这里定义一个抽象类注入了读库列表,所有的实现类从该类中继承即可。
- public class PollingLoad extends AbstractSlaveLoad {
- private int index = 0 ;
- private int size = 1 ;
- @PostConstruct
- public void init() {
- size = slaveDataSources.size() ;
- }
- @Override
- public int load() {
- int n = index ;
- synchronized (this) {
- index = (++index) % size ;
- }
- return n ;
- }
- }
配置成Bean
- @Bean
- @ConditionalOnMissingBean
- public BaseSlaveLoad slaveLoad() {
- return new PollingLoad() ;
- }
- @Bean
- public DataSourceHolder dataSourceHolder() {
- return new DataSourceHolder() ;
- }
6.数据源AOP
- public class DataSourceAspect implements MethodInterceptor {
- private DataSourceHolder holder ;
- public DataSourceAspect(DataSourceHolder holder) {
- this.holder = holder ;
- }
- @Override
- public Object invoke(MethodInvocation invocation) throws Throwable {
- Method method = invocation.getMethod() ;
- String methodName = method.getName() ;
- SlaveDB slaveDB = method.getAnnotation(SlaveDB.class) ;
- if (slaveDB == null) {
- slaveDB = method.getDeclaringClass().getAnnotation(SlaveDB.class) ;
- }
- if (methodName.startsWith("find")
- || methodName.startsWith("get")
- || methodName.startsWith("query")
- || methodName.startsWith("select")
- || methodName.startsWith("list")
- || slaveDB != null) {
- holder.set(1) ;
- } else {
- holder.set(0) ;
- }
- return invocation.proceed();
- }
- }
应该切点需要动态配置,所以这里采用spring aop的方式来配置
- @Bean
- public AspectJExpressionPointcutAdvisor logAdvisor(RWDataSourceProperties props, DataSourceHolder holder) {
- AspectJExpressionPointcutAdvisor advisor = new AspectJExpressionPointcutAdvisor() ;
- logger.info("执行表达式:{}", props.getPointcut()) ;
- advisor.setExpression(props.getPointcut()) ;
- advisor.setAdvice(new DataSourceAspect(holder)) ;
- return advisor ;
- }
7.Enable开启功能
- public class RWImportSelector implements ImportSelector {
- @Override
- public String[] selectImports(AnnotationMetadata importingClassMetadata) {
- return new String[] {RWConfig.class.getName()} ;
- }
- }
这里的RWConfig为我们上面的配置类
- @Retention(RetentionPolicy.RUNTIME)
- @Target(ElementType.TYPE)
- @Documented
- @Import({RWImportSelector.class})
- public @interface EnableRW {
- }
- @Documented
- @Retention(RUNTIME)
- @Target({ TYPE, METHOD })
- public @interface SlaveDB {
- }
有@SlaveDB的注解方法会类都会从读库中操作。
到此读写分离组件开发完成。
8.打包安装到本地仓库
- mvn install -Dmaven.test.skip=true
9.新建base-web项目
引入依赖
- <dependency>
- <groupId>com.pack</groupId>
- <artifactId>xg-component-rw</artifactId>
- <version>1.0.0</version>
- </dependency>
启动类添加注解开启读写分离功能
- @SpringBootApplication
- @EnableRW
- public class BaseWebApplication {
- public static void main(String[] args) {
- SpringApplication.run(BaseWebApplication.class, args);
- }
- }
测试:
第一次查询:
第二次查询:
为了区别两个从库设置不同的数据
这里是写库
完毕!!!