使用SpringAOP的特性实现MySQL的读写分离
前提
首先要配备MySQL的主库和从库,关于怎么配可以参考MySQL主从搭配的文章。
用于解决什么方案?
主要是解决数据库压力的问题,一个网站基本上读数据的频率会比写入数据的要多,如果分开多个数据库,有负责写入的也有负责只读的,这样多个数据的效率肯定是会比一个数据库的效率快的多。
实现的思路
- 在项目中配备两个数据源,主节点(master)只负责写入数据,从节点(slave)负责读取数据。
- 写一个只读的注解,并且使用SpringAOP特性,使用注解之前,把数据源切换成只读的数据库。
代码实现:
1.配置数据源
从配置中读取数据源文件
druid.master是配置文件中的主节点负责写入数据
druid.slave是配置文件中的从节点负责读取只读数据
初始化数据源之前,我把这两个数据源加载到IOC容器中。
package com.chenzhipeng.config.database;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
*
* <p>Title: DataSourceConfiguration</p>
* <p>Description:数据源配置</p>
* @version V1.0
* @author ZhiPeng_Chen
* @date: 2017年8月23日
*/
@Configuration//Spring的配置类
@EnableTransactionManagement
public class DataSourceConfiguration {
private static Logger LOGGER = LoggerFactory.getLogger(DataSourceConfiguration.class);
/**
* 读取配置文件中的数据驱动(阿里连接池已实现DataSource接口)
*/
@Value("${druid.type}")
private Class<? extends DataSource> dataSourceType;
/**
* 主节点
* @return
* @throws SQLException
*/
@Bean(name = "masterDataSource")
@Primary
@ConfigurationProperties(prefix = "druid.master")
public DataSource masterDataSource() throws SQLException{
DataSource masterDataSource = DataSourceBuilder.create().type(dataSourceType).build();
LOGGER.info("========加载写入节点=========", masterDataSource);
return masterDataSource;
}
/**
* 从节点
* @return
*/
@Bean(name = "slaveDataSource")
@ConfigurationProperties(prefix = "druid.slave")
public DataSource slaveDataSource(){
DataSource slaveDataSource = DataSourceBuilder.create().type(dataSourceType).build();
LOGGER.info("========加载只读节点=========", slaveDataSource);
return slaveDataSource;
}
}
2.把数据源交给mybatis
主要看roundRobinDataSourceProxy()方法
在调用这个方法,我会通过自己的主从上下文来进行配置,这里把两个数据源都放入到map中。
ReadWriteSplitRoutingDataSource 这个类主要就是用来装载这两个数据源
package com.chenzhipeng.config.database;
import javax.annotation.Resource;
import javax.sql.DataSource;
import org.apache.ibatis.mapping.DatabaseIdProvider;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.aspectj.apache.bcel.util.ClassLoaderRepository.SoftHashMap;
import org.mybatis.spring.boot.autoconfigure.ConfigurationCustomizer;
import org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration;
import org.mybatis.spring.boot.autoconfigure.MybatisProperties;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ResourceLoader;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import java.util.List;
/**
*
* <p>Title: MyBatisConfiguration</p>
* <p>Description:交给mybatis读取数据源的主从节点</p>
* @version V1.0
* @author ZhiPeng_Chen
* @date: 2017年8月23日
*/
@Configuration
@AutoConfigureAfter({DataSourceConfiguration.class})
public class MyBatisConfiguration extends MybatisAutoConfiguration{
/**
* 注入主节点
*/
@Resource(name="masterDataSource")
private DataSource masterDataSource;
/**
* 注入从节点
*/
@Resource(name="slaveDataSource")
private DataSource slaveDataSource;
public MyBatisConfiguration(MybatisProperties properties, ObjectProvider<Interceptor[]> interceptorsProvider, ResourceLoader resourceLoader, ObjectProvider<DatabaseIdProvider> databaseIdProvider, ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider) {
super(properties, interceptorsProvider, resourceLoader, databaseIdProvider, configurationCustomizersProvider);
}
@Bean(name="sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory() throws Exception{
return super.sqlSessionFactory(roundRobinDataSourceProxy());
}
@SuppressWarnings("unchecked")
public AbstractRoutingDataSource roundRobinDataSourceProxy(){
//设置数据源
ReadWriteSplitRoutingDataSource dataSource = new ReadWriteSplitRoutingDataSource();
//默认使用主数据源
dataSource.setDefaultTargetDataSource(masterDataSource);
SoftHashMap targetDataSource = new SoftHashMap();
//设置主
targetDataSource.put(DataBaseContextHoder.DataBaseType.MASTER, masterDataSource);
//设置从
targetDataSource.put(DataBaseContextHoder.DataBaseType.SLAVE, slaveDataSource);
//两数据源存入到map中
dataSource.setTargetDataSources(targetDataSource);
return dataSource;
}
}
3.配置数据源容器对象
从上一步介绍了ReadWriteSplitRoutingDataSource这个类用于装载两个数据源的信息
它默认是主节点。(既然默认是主库,能不能调用之前改变成从库?想到的就是切面编程)
package com.chenzhipeng.config.database;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
*
* <p>Title: ReadWriteSplitRoutingDataSource</p>
* <p>Description:读写分离容器</p>
* @version V1.0
* @author ZhiPeng_Chen
* @date: 2017年8月23日
*/
public class ReadWriteSplitRoutingDataSource extends AbstractRoutingDataSource{
/**
* 获取当前数据源
*/
@Override
protected Object determineCurrentLookupKey() {
return DataBaseContextHoder.getDataBaseType();
}
}
4.使用AOP实现数据源的读写分离(核心部分)
以readOnlyConnection这个注解作为切入点,当使用了这个注解之前就把默认的主节点(写入数据节点)切换为从节点(只读数据节点)。
从而实现了读写分离,只要在方法中使用@ReadOnlyConnection就是使用从节点的数据库。
package com.chenzhipeng.config.database;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
/**
* <p>Title: ReadOnlyInterceptor</p>
* <p>Description:经过切入点语法使用注释之前改变</p>
* @version V1.0
* @author ZhiPeng_Chen
* @date: 2017年8月23日
*/
@Aspect
@Component
public class ReadOnlyInterceptor implements Ordered{
public static final Logger LOGGER = LoggerFactory.getLogger(ReadOnlyInterceptor.class);
@Around(value="@annotation(readOnlyConnection)")
public Object proceed(ProceedingJoinPoint proceedingJoinPoint, ReadOnlyConnection readOnlyConnection) throws Throwable {
try {
LOGGER.info("set database connection to read only");
DataBaseContextHoder.setDataBaseType(DataBaseContextHoder.DataBaseType.SLAVE);
Object result = proceedingJoinPoint.proceed();
return result;
} finally {
DataBaseContextHoder.clearDataType();
LOGGER.info("restore database connection");
}
}
@Override
public int getOrder() {
return 0;
}
}