springboot 多数据源 读写分离 AOP方式

本文分享了在SpringBoot环境下如何进行读写分离配置,详细介绍了配置文件设置、配置类编写、AOP应用及测试结果,适用于springboot 2.1.0.RELEASE版本。

大家好,我是烤鸭:

        今天分享springboot读写分离配置。

         环境:

                 springboot  2.1.0.RELEASE

         场景说明,目前的需求是 读数据源 * 2 + 写数据源 * 1

 

1.    配置文件


    application.yml

server:
  port: 8085
spring:
  application:
    name: test-data-test
  datasource:
    write:
      jdbc-url: jdbc:mysql://localhost:3306/test
      username: root
      password: test.Dev
      driver-class-name: com.mysql.jdbc.Driver
      type: com.zaxxer.hikari.HikariDataSource   
      connectionTimeout: 30000
      validationTimeout: 5000
      maxPoolSize: 200
      minIdle: 100
    readaw:
      jdbc-url: jdbc:mysql://localhost:3306/test
      username: root
      password: test!i
      driver-class-name: com.mysql.jdbc.Driver
      type: com.zaxxer.hikari.HikariDataSource   
      connectionTimeout: 30000
      validationTimeout: 5000
      maxPoolSize: 200
      minIdle: 100
    readdc:
      jdbc-url: jdbc:mysql://localhost:3306/test
      username: root
      password: test!i
      driver-class-name: com.mysql.jdbc.Driver
      type: com.zaxxer.hikari.HikariDataSource   
      connectionTimeout: 30000
      validationTimeout: 5000
      maxPoolSize: 200
      minIdle: 100
#mybatis
mybatis:
  ###把xml文件放在com.XX.mapper.*中可能会出现找到的问题,这里把他放在resource下的mapper中
  mapper-mapperLocations: classpath*:mapper/**/**/*.xml
  type-aliases-package: com.test.test.pojo
  configuration:
    map-underscore-to-camel-case: true
    cache-enabled: false
    call-setters-on-nulls: true
    useGeneratedKeys: 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 {

    @Autowired
    Environment 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;
    }

    @Bean
    public 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;

    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(myRoutingDataSource);
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/**/*.xml"));
        return sqlSessionFactoryBean.getObject();
    }

    @Bean
    public 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){
            //获取注解的value
            DbSource 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
    @Override
    protected 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方法中使用查询(不同的库)后插入操作,结果如图所示。

 

Spring Boot 中实现多数据源读写分离通常通过AOP(面向切面编程)和数据源切换工具如`spring-jdbc-datasource-proxy`或者自定义拦截器来完成。以下是步骤概述: 1. 配置多个数据源:在`application.yml`或`application.properties`文件中配置两个或更多的数据源,每个数据源代表读库和写库。 ```yaml spring: datasource: db1: url: jdbc:mysql://read_host:port/read_db?serverTimezone=UTC username: read_user password: read_password db2: url: jdbc:mysql://write_host:port/write_db?serverTimezone=UTC username: write_user password: write_password ``` 2. 使用AOP管理事务:利用AOP代理技术,在需要执行数据库操作的地方创建一个切入点,比如所有的`@Repository`注解的DAO类。可以编写一个Advisor来在方法执行前后自动切换数据源。 ```java @Component @Aspect public class DataSourceSwitchingAspect { @Before("execution(* com.example.repository..*(..))") public void switchToWriteDataSource(JoinPoint joinPoint) { // 切换到write数据源 PlatformTransactionManager transactionManager = (PlatformTransactionManager) context.getBean(PlatformTransactionManager.class); TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition()); DataSource originalDataSource = DataSourceContextHolder.getDataSource(); DataSource writeDataSource = getWriteDataSource(); // 获取写入数据源 DataSourceContextHolder.setDataSource(writeDataSource); try { // 执行业务代码 } finally { DataSourceContextHolder.clearDataSource(); transactionManager.rollback(status); // 这里可以根据实际情况选择提交或回滚 } } private DataSource getWriteDataSource() { ... } // 根据实际需求获取写数据源 } ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

烤鸭的世界我们不懂

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值