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();@Overrideprotected 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 dbsourcespring.datasource.type=com.alibaba.druid.pool.DruidDataSourcespring.datasource.driver-class-name=oracle.jdbc.driver.OracleDriverspring.datasource.url=jdbc:oracle:thin:@xxx.xxx.xxx.7:1521:xxxspring.datasource.username=xxxspring.datasource.password=xxx# slave dbsourcespring.slave-datasource.type=com.alibaba.druid.pool.DruidDataSourcespring.slave-datasource.driver-class-name=oracle.jdbc.driver.OracleDriverspring.slave-datasource.url=jdbc:oracle:thin:@xxx.xxx.xxx.6:1521:xxxspring.slave-datasource.username=xxxspring.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*/@Configurationpublic 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})// 说明该注解可以写在类和方法上@Documentedpublic @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之前运行@Componentpublic 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);}
