首页
搜索 搜索
当前位置:综合 > 正文

SpringBoot读写分离组件开发详解

2023-07-07 09:32:22 实战案例锦集

实现目标:一写多读,读可以任意配置多个,默认都是从写库中进行操作,只有符合条件的方法(指定的目标方法或者标有指定注解的方法才会从读库中操作)。独立打成一个jar包放入本地仓库。

实现原理:通过aop。


(资料图片仅供参考)

pom.xml配置文件
  org.springframework.boot  spring-boot-starter  org.springframework.boot  spring-boot-starter-data-jpa  org.springframework.boot  spring-boot-configuration-processor  true
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集合)。

属性配置类
@Component@ConfigurationProperties(prefix = "pack.datasource")public class RWDataSourceProperties {    private String pointcut ;  private HikariConfig master ;  private List slaves = new ArrayList<>();  }
读写配置类
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 slaveDataSources(RWDataSourceProperties rwDataSourceProperties) {    List 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 slaveDataSources) {    BaseRoutingDataSource ds = new BaseRoutingDataSource() ;    Map 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 ;  }  }
数据源路由
public class BaseRoutingDataSource extends AbstractRoutingDataSource {  @Resource  private DataSourceHolder holder;    @Override  protected Object determineCurrentLookupKey() {    return holder.get() ;  }  }
public class DataSourceHolder {    private ThreadLocal context = new ThreadLocal() {    @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 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@ConditionalOnMissingBeanpublic BaseSlaveLoad slaveLoad() {  return new PollingLoad() ;}  @Beanpublic DataSourceHolder dataSourceHolder() {  return new DataSourceHolder() ;}
数据源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的方式来配置

@Beanpublic 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 ;}
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的注解方法或类都会从读库中操作。

到此读写分离组件开发完成。

打包安装到本地仓库
mvn install -Dmaven.test.skip=true
新建base-web项目

引入依赖

  com.pack  xg-component-rw  1.0.0

启动类添加注解开启读写分离功能

@SpringBootApplication@EnableRWpublic class BaseWebApplication {  public static void main(String[] args) {    SpringApplication.run(BaseWebApplication.class, args);  }}

测试:

第一次查询:

图片

第二次查询:

图片

为了区别两个从库设置不同的数据

这里是写库