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);
}