SpringBoot MySQL读写分离
MySQL数据库安装请参考:CentOS7安装MySQL数据库及MariaDB
MySQL数据库主从复制环境搭建请参考:CentOS7实现MySQL读写分离环境搭建
读写分离实现
读写分离要做的事情就是对于一条SQL该选择哪个数据库去执行,至于谁来做选择数据库这件事儿,无非两个,要么中间件帮我们做,要么程序自己做。因此,一般来讲,读写分离有两种实现方式。第一种是依靠中间件(比如:MyCat),也就是说应用程序连接到中间件,中间件帮我们做SQL分离;第二种是应用程序自己去做分离。
编码思想
所谓的手写读写分离,需要用户自定义一个动态的数据源,该数据源可以根据当前上下文中调用方法是读或者是写方法决定返回主库的链接还是从库的链接。这里我们使用Spring提供的一个代理数据源AbstractRoutingDataSource接口。
该接口需要用户完善一个determineCurrentLookupKey抽象法,系统会根据这个抽象返回值决定使用系统中定义的数据源。
@Nullable
protected abstract Object determineCurrentLookupKey();
其次该类还有两个属性需要指定defaultTargetDataSource
和targetDataSources
,其中defaultTargetDataSource需要指定为Master数据源。targetDataSources是一个Map需要将所有的数据源添加到该Map中,以后系统会根据determineCurrentLookupKey方法的返回值作为key从targetDataSources查找相应的实际数据源。如果找不到则使用defaultTargetDataSource指定的数据源。
实现步骤
添加pom依赖
<!--Junit测试-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!--MySQL & MyBatis-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
application.properties配置
server.port=8888
server.servlet.context-path=/
# 主节点数据源配置
spring.datasource.master.username=root
spring.datasource.master.password=root
spring.datasource.master.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.master.jdbc-url=jdbc:mysql://CentOS:3306/dora?useUnicode=true&characterEncoding=UTF8&serverTimezone=UTC&useSSL=false
# 从节点1数据源配置
spring.datasource.slave1.username=root
spring.datasource.slave1.password=root
spring.datasource.slave1.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.slave1.jdbc-url=jdbc:mysql://CentOSA:3306/dora?useUnicode=true&characterEncoding=UTF8&serverTimezone=UTC&useSSL=false
# 从节点2数据源配置
spring.datasource.slave2.username=root
spring.datasource.slave2.password=root
spring.datasource.slave2.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.slave2.jdbc-url=jdbc:mysql://CentOSB:3306/dora?useUnicode=true&characterEncoding=UTF8&serverTimezone=UTC&useSSL=false
# 从节点3数据源配置
spring.datasource.slave2.username=root
spring.datasource.slave2.password=root
spring.datasource.slave2.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.slave2.jdbc-url=jdbc:mysql://CentOSC:3306/dora?useUnicode=true&characterEncoding=UTF8&serverTimezone=UTC&useSSL=false
配置数据源
/**
* 该类是自定义数据源,由于必须将系统的数据源给替换掉。
*/
@Configuration
public class UserDefineDatasourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.slave1")
public DataSource slave1DataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.slave2")
public DataSource slave2DataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.slave3")
public DataSource slave3DataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public DataSource proxyDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
@Qualifier("slave1DataSource") DataSource slave1DataSource,
@Qualifier("slave2DataSource") DataSource slave2DataSource,
@Qualifier("slave3DataSource") DataSource slave3DataSource) {
DynamicDataSource proxy = new DynamicDataSource();
proxy.setDefaultTargetDataSource(masterDataSource);//设置默认数据源
Map<Object, Object> mappedDataSource = new HashMap<>();
mappedDataSource.put("master", masterDataSource);
mappedDataSource.put("slave-01", slave1DataSource);
mappedDataSource.put("slave-02", slave2DataSource);
mappedDataSource.put("slave-03", slave3DataSource);
proxy.setTargetDataSources(mappedDataSource); //注册所有数据源
return proxy;
}
/**
* 当自定义数据源,用户必须覆盖SqlSessionFactory创建
*
* @param dataSource
* @return
* @throws Exception
*/
@Bean
public SqlSessionFactory sqlSessionFactory(@Qualifier("proxyDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setTypeAliasesPackage("com.Dora.entities");
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mappers/*.xml"));
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBean.getObject();
return sqlSessionFactory;
}
/**
* 当自定义数据源,用户必须覆盖SqlSessionTemplate,开启BATCH处理模式
*
* @param sqlSessionFactory
* @return
*/
@Bean
public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory, ExecutorType.BATCH);
}
/***
* 当自定义数据源,用户必须注入,否则事务控制不生效
* @param dataSource
* @return
*/
@Bean
public PlatformTransactionManager platformTransactionManager(@Qualifier("proxyDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
配置切面类
/**
* 用户自定义切面,负责读取SlaveDB注解,并且在DBTypeContextHolder中设置读写类型
*/
@Aspect
@Order(0) //控制切面顺序,保证在事务切面之前运行切面
@Component
public class ServiceMethodAOP {
private static final Logger logger = LoggerFactory.getLogger(ServiceMethodAOP.class);
@Around("execution(* com.Dora.service..*.*(..))")
public Object methodInterceptor(ProceedingJoinPoint pjp) {
Object result = null;
try {
//获取当前的方法信息
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
Method method = methodSignature.getMethod();
//判断方法上是否存在注解@SlaveDB
boolean present = method.isAnnotationPresent(SlaveDB.class);
OperType operType = null;
if (!present) {
operType = OperType.WRIRTE;
} else {
operType = OperType.READ;
}
OperTypeContextHolder.setOperType(operType);
logger.debug("当前操作:" + operType);
result = pjp.proceed();
//清除线程变量
OperTypeContextHolder.clear();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return result;
}
}
用到的其他类
动态数据源
/**
* 该类属于代理数据源,负责负载均衡
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
private static final Logger logger = LoggerFactory.getLogger(DynamicDataSource.class);
private String masterDBKey = "master";
private List<String> slaveDBKeys = Arrays.asList("slave-01", "slave-02", "slave-03");
private static final AtomicInteger round = new AtomicInteger(0);
/**
* 需要在该方法中,判断当前用户的操作是读操作还是写操作
*
* @return
*/
@Override
protected Object determineCurrentLookupKey() {
String dbKey = null;
OperType operType = OperTypeContextHolder.getOperType();
if (operType.equals(OperType.WRIRTE)) {
dbKey = masterDBKey;
} else {
//轮询返回 0 1 2 3 4 5 6
int value = round.getAndIncrement();
if (value < 0) {
round.set(0);
}
Integer index = round.get() % slaveDBKeys.size();
dbKey = slaveDBKeys.get(index);
}
logger.debug("当前的DBkey:" + dbKey);
return dbKey;
}
}
读写类型
/**
* 写、读类型
*/
public enum OperatorTypeEnum {
WRITE, READ;
}
记录操作类型
/**
* 该类主要是用于存储,当前用户的操作类型,将当前的操作存储在当前线程的上下文中
*/
public class OPTypeContextHolder {
private static final ThreadLocal<OperatorTypeEnum> OPERATOR_TYPE_THREAD_LOCAL = new ThreadLocal<>();
public static void set(OperatorTypeEnum dbType) {
OPERATOR_TYPE_THREAD_LOCAL.set(dbType);
}
public static OperatorTypeEnum get() {
return OPERATOR_TYPE_THREAD_LOCAL.get();
}
public static void clear(){
OPERATOR_TYPE_THREAD_LOCAL.remove();
}
}
业务方法标记注解
/**
* 该注解用于标注,当前用户的调用方法是读还是写
*/
@Retention(RetentionPolicy.RUNTIME) //表示运行时解析注解
@Target(value = {ElementType.METHOD})//表示只能在方法上加
public @interface SlaveDB { }