天天看点

SpringBoot 实现 Oracle 主从数据库的动态切换,并实现读写分离

1、问题提出

目前 Oracle 中有两个数据库,要实现一个数据库只进行读操作,另一个数据库进行写操作,也即数据库的读写分离,该怎么做?

2、简要说明:本问题与【主从复制、读写分离】还不太一样

主从复制、读写分离一般是一起使用的。目的很简单,就是为了提高数据库的并发性能。你想,假设是单机,读写都在一台 MySQL 上面完成,性能肯定不高。如果有三台MySQL,一台 mater 只负责写操作,两台 salve 只负责读操作,性能不就能大大提高了吗?

所以主从复制、读写分离就是为了数据库能支持更大的并发。

随着业务量的扩展,如果是单机部署的 MySQL,会导致I/O频率过高。采用主从复制、读写分离可以提高数据库的可用性。

而本问题只需要实现主从数据库的动态切换即可。

3、AbstractRoutingDataSource

SpringBoot 提供了 AbstractRoutingDataSource,可以根据用户自定义的规则选择当前的数据源,这样我们每次访问数据库之前,设置要使用的数据源,就可以实现数据源的动态切换。

4、具体实现

1、创建一个类继承 AbstractRoutingDataSource

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * 1. 创建 RoutingDataSource 继承 AbstractRoutingDataSource
 * 重写 determineCurrentLookupKey方法,返回要使用的数据源key值。
 */
public class RoutingDataSource extends AbstractRoutingDataSource {
    private Logger logger = LogManager.getLogger();

    @Override
    protected Object determineCurrentLookupKey() {
        String dataSource = RoutingDataSourceHolder.getDataSource();
        logger.info("使用数据源:{}", dataSource);
        return dataSource;
    }
}
           
2、创建一个管理数据源 key 值的类,RoutingDataSourceManager
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

/**
 * 2. 创建一个管理数据源key值的类 RoutingDataSourceHolder
 * 代码设置了一个事务内使用同一个数据源。
 */
public class RoutingDataSourceManager {

    private static Logger logger = LogManager.getLogger();

    private static final ThreadLocal<String> dataSources = new ThreadLocal<>();

    // 一个事务内使用同一个数据源
    public static void setDataSource(String dataSourceName) {
        if (dataSources.get() == null) {
            dataSources.set(dataSourceName);
            logger.info("设置数据源:{}", dataSourceName);
        }
    }

    public static String getDataSource() {
        return dataSources.get();
    }

    public static void clearDataSource() {
        dataSources.remove();
    }

}
           

3、application.properties

# OracleDbProperties  
# master dbsource
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=oracle.jdbc.driver.OracleDriver
spring.datasource.url=jdbc:oracle:thin:@xxx.xxx.xxx.7:1521:xxx
spring.datasource.username=xxx
spring.datasource.password=xxx

# slave dbsource
spring.slave-datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.slave-datasource.driver-class-name=oracle.jdbc.driver.OracleDriver
spring.slave-datasource.url=jdbc:oracle:thin:@xxx.xxx.xxx.6:1521:xxx
spring.slave-datasource.username=xxx
spring.slave-datasource.password=xxx
           

4、配置主从数据库

import com.alibaba.druid.pool.DruidDataSource;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * 配置主从数据库:主:xxx.xxx.xxx.7;从:xxx.xxx.xxx.6
 */
@Configuration
public class DataSourceConfigurer {
    private Logger logger = LogManager.getLogger();

    public final static String MASTER_DATASOURCE = "masterDataSource";// 主数据库
    public final static String SLAVE_DATASOURCE = "slaveDataSource";// 从数据库

    // 主数据库:.7
    @Bean(MASTER_DATASOURCE)
    @ConfigurationProperties(prefix = "spring.datasource")
    public DruidDataSource masterDataSource(DataSourceProperties properties) {
        DruidDataSource build = properties.initializeDataSourceBuilder().type(DruidDataSource.class).build();
        logger.info("配置主数据库:{}", build);
        return build;
    }

    // 从数据库:.6
    @Bean(SLAVE_DATASOURCE)
    @ConfigurationProperties(prefix = "spring.slave-datasource")
    public DruidDataSource slaveDataSource(DataSourceProperties properties) {
        DruidDataSource build = properties.initializeDataSourceBuilder().type(DruidDataSource.class).build();
        logger.info("配置从数据库:{}", build);
        return build;
    }

    /**
     * Primary   优先使用该Bean
     * DependsOn 先执行主从数据库的配置
     * Qualifier 指定使用哪个Bean
     *
     * @param masterDataSource 主数据源
     * @param slaveDataSource  从数据源
     * @return
     */
    @Bean
    @Primary
    @DependsOn(value = {MASTER_DATASOURCE, SLAVE_DATASOURCE})
    public DataSource routingDataSource(@Qualifier(MASTER_DATASOURCE) DruidDataSource masterDataSource,
                                        @Qualifier(SLAVE_DATASOURCE) DruidDataSource slaveDataSource) {
        if (StringUtils.isBlank(slaveDataSource.getUrl())) {
            logger.info("没有配置从数据库,默认使用主数据库");
            return masterDataSource;
        }

        // 设置初始化targetDataSources对象
        Map<Object, Object> map = new HashMap<>();
        map.put(DataSourceConfigurer.MASTER_DATASOURCE, masterDataSource);
        map.put(DataSourceConfigurer.SLAVE_DATASOURCE, slaveDataSource);
        RoutingDataSource routing = new RoutingDataSource();
        // 设置动态数据源
        routing.setTargetDataSources(map);
        // 设置默认数据源
        routing.setDefaultTargetDataSource(masterDataSource);
        logger.info("主从数据库配置完成");
        return routing;
    }
}
           

5、自定义注解和切面类

自定义注解:

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)// 声明注解有效的时间
@Target({ElementType.TYPE, ElementType.METHOD})// 说明该注解可以写在类和方法上
@Documented
public @interface DataSourceWith {
    String key() default "";
}
           

切面类:

<!-- 添加aop依赖 -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
           
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Aspect
@Order(-1)// 保证该AOP在@Transactional之前运行
@Component
public class DataSourceWithAspect {

    /**
     * 使用DataSourceWith注解就拦截
     */
    @Pointcut("@annotation(cn.edu.zzuli.hnsmz.annotation.DataSourceWith)||@within(cn.edu.zzuli.hnsmz.annotation.DataSourceWith)")
    public void doPointcut() {

    }

    /**
     * 方法前,为了在事务前设置
     */
    @Before("doPointcut()")
    public void doBefore(JoinPoint joinPoint) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        // 获取注解对象
        DataSourceWith dataSource = method.getAnnotation(DataSourceWith.class);
        if (dataSource == null) {
            // 方法没有就获取类上的
            dataSource = method.getDeclaringClass().getAnnotation(DataSourceWith.class);
        }
        String key = dataSource.key();
        RoutingDataSourceHolder.setDataSource(key);
    }

    @After("doPointcut()")
    public void doAfter(JoinPoint joinPoint) {
        RoutingDataSourceHolder.clearDataSource();
    }

}
           
6、使用
@DataSourceWith(key = DataSourceConfigurer.SLAVE_DATASOURCE)
public Long selectById(String id) {
    return studentService.selectById(id);
}
           

参考自:https://blog.csdn.net/m0_68615056/article/details/123738282