文章目录

  • 场景,理解
  • 解决方案
  • 数据源配置文件
  • 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");
  }

}

测试:

就一个普通的查询:

springboot实现读写分离_spring


dataSource 切换到,是在切换里面打印的:

springboot实现读写分离_数据源_02

源码项目

https://gitee.com/stackR/springboot-yida-plus