文章目录
- 场景,理解
- 解决方案
- 数据源配置文件
- mybatis配置文件
- AbstractRoutingDataSource详解
- aop切面
- 测试:
- 源码项目
场景,理解
个人的理解:
当响应的瓶颈在数据库的时候,就要考虑数据库的读写分离,当然还可以分库分表,那是单表数据量特别大,当单表数据量不是特别大,但是请求量比较大的时候,就要考虑读写分离了.具体的话,还是要看自己的业务…如果还是很慢,那就要分库分表了…我们这篇就简单讲一下读写分离
解决方案
不使用框架,通过springboot框架自己编写读写分离代码;
数据源配置文件
因为要配置多个数据源,所以mybatis的配置文件要自己配置了:
首先创建数据源,一主两从:
yml配置文件:
spring:
datasource:
url: jdbc:mysql://localhost:3306/test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: root
slave1:
url: jdbc:mysql://localhost:3306/test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true
username: reader1
password: reader1
slave2:
url: jdbc:mysql://localhost:3306/test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true
username: reader2
password: reader2
创建数据源配置文件类:
import java.util.ArrayList;
import java.util.List;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class DataBaseConfiguration {
private static final Logger logger = LoggerFactory.getLogger(DataBaseConfiguration.class);
@Value("${spring.datasource.type}")
private Class<? extends DataSource> dataSourceType;
@Bean(name = "writeDataSource")
// @Primary
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource writeDataSource() {
logger.info("-------------------- writeDataSource init ---------------------");
DataSource dataSource = DataSourceBuilder.create().type(dataSourceType).build();
return dataSource;
}
/**
* 有多少个从库就要配置多少个
*
* @return
*/
@Bean(name = "readDataSource1")
@ConfigurationProperties(prefix = "spring.slave1")
public DataSource readDataSourceOne() {
logger.info("-------------------- readDataSourceOne init --------------------");
return DataSourceBuilder.create().type(dataSourceType).build();
}
@Bean(name = "readDataSource2")
@ConfigurationProperties(prefix = "spring.slave2")
public DataSource readDataSourceTwo() {
logger.info("-------------------- readDataSourceTwo init --------------------");
return DataSourceBuilder.create().type(dataSourceType).build();
}
@Bean("readDataSources")
public List<DataSource> readDataSources() {
List<DataSource> dataSources = new ArrayList<>();
dataSources.add(readDataSourceOne());
dataSources.add(readDataSourceTwo());
return dataSources;
}
}
mybatis配置文件
创建mybatis配置文件:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<!-- map下划线自动转大写 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!-- 打印查询语句 -->
<setting name="logImpl" value="STDOUT_LOGGING"/>
<!-- 返回字段为空时null也显示该字段 -->
<setting name="callSettersOnNulls" value="true"/>
</settings>
<!-- 别名定义 -->
<typeAliases>
<!-- 批量别名定义,指定包名,mybatis自动扫描包中的po类,自动定义别名,别名是类名(首字母大写或小写都可以,一般用小写) -->
<!-- <package name="com.hotpot..pojo" /> -->
<!--<typeAlias type="com.hotpot.sys.pojo.SysUser" alias="sysUser"/> -->
<!-- 批量别名定义
指定包名,mybatis自动扫描包中的po类,自动定义别名,别名就是类名(首字母大写或小写都可以)
-->
<package name="com.study.springbootplus.domain.entity"/>
</typeAliases>
</configuration>
然后创建SqlSessionFactory,然后创建将AbstractRoutingDataSource类加入spring中:
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Resource;
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import com.study.springbootplus.domain.enums.DataSourceType;
@Configuration
@ConditionalOnClass({EnableTransactionManagement.class})
@Import({DataBaseConfiguration.class})
public class MybatisConfiguration {
/**
* mybatis 配置路径
*/
@Value("${spring.mybatis.configLocation:mybatis/mybatis.xml}")
private String MYBATIS_CONFIG;
/**
* mapper路径
*/
@Value("${spring.mybatis.mapperLocations:mapper/*.xml}")
private String MAPPER_LOCATION;
@Value("${spring.datasource.type}")
private Class<? extends DataSource> dataSourceType;
@Value("2")
private String dataSourceSize;
@Resource(name = "writeDataSource")
private DataSource dataSource;
@Resource(name = "readDataSources")
private List<DataSource> readDataSources;
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
/** 设置mybatis configuration 扫描路径 */
sqlSessionFactoryBean.setConfigLocation(new ClassPathResource(MYBATIS_CONFIG));
sqlSessionFactoryBean.setDataSource(roundRobinDataSouceProxy());
//AbstractRoutingDataSource s= roundRobinDataSouceProxy();
/** 添加mapper 扫描路径 */
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources(MAPPER_LOCATION));
return sqlSessionFactoryBean.getObject();
}
/**
* 有多少个数据源就要配置多少个bean
*
* @return
*/
@Bean
public AbstractRoutingDataSource roundRobinDataSouceProxy() {
int size = Integer.parseInt(dataSourceSize);
MyAbstractRoutingDataSource proxy = new MyAbstractRoutingDataSource(size);
Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
// DataSource writeDataSource = SpringContextHolder.getBean("writeDataSource");
// 写
targetDataSources.put(DataSourceType.write.getType(), dataSource);
//1个读数据库时
// targetDataSources.put(DataSourceType.read.getType(determineCurrentLookupKey),readDataSource);
//多个读数据库时
for (int i = 0; i < size; i++) {
targetDataSources.put(i, readDataSources.get(i));
}
proxy.setDefaultTargetDataSource(dataSource);
proxy.setTargetDataSources(targetDataSources);
return proxy;
}
}
AbstractRoutingDataSource详解
AbstractRoutingDataSource类是spring提供的抽象的路由数据源类,可以在执行查询之前设置具体使用哪个数据源,我们就可以通过ThreadLocal类,然后通过aop进行设置,然后在AbstractRoutingDataSource类里面设置使用的数据源;
AbstractRoutingDataSource 源码:
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
@Nullable
//存储的数据源
private Map<Object, Object> targetDataSources;
@Nullable
//默认的数据源
private Object defaultTargetDataSource;
//如果找不到对应的数据源,是否使用默认数据源,默认为使用
private boolean lenientFallback = true;
//通过jndi获取数据库连接
private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
@Nullable
//解析后的存储的数据源
private Map<Object, DataSource> resolvedDataSources;
@Nullable
//解析后的默认数据源
private DataSource resolvedDefaultDataSource;
我们新建个枚举类,主库从库:
public enum DataSourceType {
read("read","从库"),
write("write", "主库");
private String type;
private String name;
DataSourceType(String type, String name) {
this.type = type;
this.name = name;
}
public String getType() {
return type;
}
public String getName() {
return name;
}
}
封装一个ThreadLocal类进行设置:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.study.springbootplus.domain.enums.DataSourceType;
public class DataSourceContextHolder {
private static final Logger logger = LoggerFactory.getLogger(DataSourceContextHolder.class);
private static final ThreadLocal<String> local = new ThreadLocal<String>();
public static ThreadLocal<String> getLocal() {
return local;
}
/**
* 读可能是多个库
*/
public static void read() {
logger.debug("读操作-----");
local.set(DataSourceType.read.getType());
}
/**
* 写只有一个库
*/
public static void write() {
logger.debug("写操作-----");
local.set(DataSourceType.write.getType());
}
public static void clearDB(){
local.remove();
}
public static String getJdbcType() {
return local.get();
}
}
AbstractRoutingDataSource的抽象方法determineCurrentLookupKey就是确认在执行的时候使用的数据源key,实现AbstractRoutingDataSource类,实现方法determineCurrentLookupKey方法:
import java.util.concurrent.atomic.AtomicInteger;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import com.study.springbootplus.domain.enums.DataSourceType;
public class MyAbstractRoutingDataSource extends AbstractRoutingDataSource {
private final int dataSourceNumber;
private AtomicInteger count = new AtomicInteger(0);
public MyAbstractRoutingDataSource(int dataSourceNumber) {
this.dataSourceNumber = dataSourceNumber;
}
@Override
protected Object determineCurrentLookupKey() {
String typeKey = DataSourceContextHolder.getJdbcType();
if (typeKey == null) {
//使用默认的,写数据库
return null;
}
// 写
if (typeKey.equals(DataSourceType.write.getType())) {
return DataSourceType.write.getType();
}
// 读简单负载均衡
int number = count.getAndAdd(1);
int lookupKey = number % dataSourceNumber;
return new Integer(lookupKey);
}
}
aop切面
因为spring事务也是走的aop,所以我们这个切面设置数据源的时候一定要在最前面.所以我们设置order为-1:
import java.lang.reflect.Method;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Aspect
@Order(-1)// 保证该AOP在@Transactional之前执行
@Component
public class DataSourceAop {
private static final Logger logger = LoggerFactory.getLogger(DataBaseConfiguration.class);
@Before("execution(* com.study.*.mapper..*.select*(..)) || execution(* com.study.*.mapper..*.get*(..))|| execution(* com.study.*.mapper..*.query*(..))")
public void setReadDataSourceType() {
DataSourceContextHolder.read();
logger.info("dataSource 切换到:Read-2");
}
@Before("execution(* com.study.*.mapper..*.insert*(..)) || execution(* com.study.*.mapper..*.update*(..))")
public void setWriteDataSourceType() {
DataSourceContextHolder.write();
logger.info("dataSource 切换到:Write-2");
}
@After("execution(* com.study.*.mapper..*.*(..))")
public void remove() {
DataSourceContextHolder.clearDB();
logger.info("dataSource clear-2");
}
}
测试:
就一个普通的查询:
dataSource 切换到,是在切换里面打印的:
源码项目
https://gitee.com/stackR/springboot-yida-plus