SpringBoot+Mybatis实现读写分离

最近的项目中需要实现简单的读写分离,由于项目中使用的是SpringBoot+Mybatis,简单的实现了一下功能

读写分离的概念已经提过很多次:单个数据库的承受服务请求的压力太大,于是将数据库的读操作和写操作分别执行,由于我们的应用大部分是读操作,写操作频率较低,于是常见的结构是一主多从;(当然生产环境是有主备切换的,这里不赘述)

核心思想

基于Spring的AOP,在controller层进行切面设置,根据请求,动态设置数据源;

代码实现

1.配置文件application.properties

## 主库配置
jdbc.master.url=jdbc:mysql://localhost:3306/sakila?serverTimezone=CTT
jdbc.master.driver=com.mysql.cj.jdbc.Driver
jdbc.master.username=root
jdbc.master.password=123456
## 从库配置 可配置多个
jdbc.slave0.driver=com.mysql.cj.jdbc.Driver
jdbc.slave0.url=jdbc:mysql://localhost:3306/sakila_slave?serverTimezone=CTT
jdbc.slave0.username=root
jdbc.slave0.password=123456
  1. 数据源配置类DataSourceConfig
package com.xxx.demo.master_slave.config;

import com.alibaba.druid.pool.DruidDataSource;
import com.xxx.demo.master_slave.ds.DynamicDataSource;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * <p>
 * 描述:
 *
 * @author
 * @version 1.0
 */
@Configuration
public class DataSourceConfig {
    /*
    *Spring的spel表达式,获取spring的配置变量
    */
    @Value("${jdbc.master.driver}")
    private String masterDriverName;
    @Value("${jdbc.master.url}")
    private String masterJdbcUrl;
    @Value("${jdbc.master.username}")
    private String masterUserName;
    @Value("${jdbc.master.password}")
    private String masterPassWord;

    @Value("${jdbc.slave0.driver}")
    private String slave0DriverName;
    @Value("${jdbc.slave0.url}")
    private String slave0JdbcUrl;
    @Value("${jdbc.slave0.username}")
    private String slave0UserName;
    @Value("${jdbc.slave0.password}")
    private String slave0PassWord;

    @Bean("masterDataSource")
    public DataSource getMasterDataSource(){
        return  getDataSource(masterDriverName,masterJdbcUrl,masterUserName,masterPassWord);
    }

    @Bean("slave0DataSource")
    public DataSource getSlave0DataSource(){
        return  getDataSource(slave0DriverName,slave0JdbcUrl,slave0UserName,slave0PassWord);
    }

    @Bean("dynamicDataSource")
    public DataSource getDyamicDataSource(){
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("master",getMasterDataSource());
        targetDataSources.put("slave0",getSlave0DataSource());
        DynamicDataSource dataSource = new DynamicDataSource(targetDataSources);
        dataSource.setDefaultTargetDataSource(getMasterDataSource());
        return dataSource;
    }

    @Bean
    public SqlSessionFactory getSqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dataSource) throws Exception {
        // 配置MapperConfig
        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        // 这个配置使全局的映射器启用或禁用缓存
        configuration.setCacheEnabled(true);
         //配置默认的执行器:
        configuration.setDefaultExecutorType(ExecutorType.REUSE);
        // 全局启用或禁用延迟加载,禁用时所有关联对象都会即时加载
        configuration.setLazyLoadingEnabled(false);
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        sqlSessionFactoryBean.setConfiguration(configuration);
        return sqlSessionFactoryBean.getObject();
    }

    private DataSource getDataSource(String driver,String jdbcUrl,String userName,String password){
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName(driver);
        dataSource.setUrl(jdbcUrl);
        dataSource.setUsername(userName);
        dataSource.setPassword(password);
        return  dataSource;
    }
}

数据源配置类中配置的数据源是Alibaba的DruidDataSource,生成masterDataSource和slave0DataSource两个数据源,并且将其注入我自己写的DynamicDataSource类中

注意:getSqlSessionFactory()方法必须配置。SpringBoot提供的自动配置,会帮助我们配置sqlSession或者SqlSessionFactory等类,但是前提是使用Spring默认提供的数据源,此时数据源已经改变,所以需要我们手动配置。

3.动态数据源类DynamicDataSource

package com.xxx.demo.master_slave.ds;

import com.xxx.demo.master_slave.context.DataSourceContextHolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.util.Assert;

import javax.sql.DataSource;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * <p>
 * 描述:
 * 版本1.0: 2019/10/17 14:44 新建
 * </p>
 *
 * @author 
 * @version 1.0
 */

public class DynamicDataSource extends AbstractRoutingDataSource {

    private static final Logger logger = LoggerFactory.getLogger(DynamicDataSource.class);

    //做从库负载均衡用
    private AtomicInteger integer = new AtomicInteger(0);

    private DataSource writeDataSource;

    private List<DataSource> readDataSources = new ArrayList<>();


    private int readDataSourceNum;

    public DynamicDataSource(Map<Object, Object> targetDataSources){
        Assert.notEmpty(targetDataSources,"至少配置一个数据源");
        if(targetDataSources.size() == 1){
            writeDataSource = (DataSource) targetDataSources.values().iterator().next();
            return;
        }
        for (Map.Entry<Object, Object> entry : targetDataSources.entrySet()) {
            if (entry.getKey().toString().toLowerCase().contains("master")) {
                if (writeDataSource != null) {
                    throw new IllegalStateException("写数据源只能定义一个");
                }
                this.writeDataSource = (DataSource)entry.getValue();
                continue;
            }
            //若数据源名称不含master,则默认为读数据源
            readDataSources.add((DataSource)entry.getValue());
        }
        if (writeDataSource == null) {
            throw new IllegalStateException("至少需要定义一个写数据源");
        }
        readDataSourceNum = readDataSources.size();
        //多数据源配置
        setTargetDataSources(targetDataSources);
    }

    /**
    * 获取数据源的key,即配置类中的master/slave0
    */
    @Override
    protected Object determineCurrentLookupKey() {
        //数据源类型上下文中获取数据源类型
        String type = DataSourceContextHolder.getDataSourceType();
        if("slave".equals(type) && readDataSourceNum > 0){
            if(integer.get() > 100000){
                integer.set(0);
            }
            logger.info("计数器的值:{}",integer.get());
            int index = integer.getAndIncrement() % readDataSourceNum;
            type = type + index;
        }
        logger.info("数据源类型:{}",type);
        return type;
    }

}

AbstractRoutingDataSource类是Spring提供的多数据源配置的基础类,允许我们继承并且实现自己的查找数据源的逻辑;determineCurrentLookupKey()方法是查找我们配置的数据源的key,即master/slave0等等;
4.数据源类型上下文类

package com.xxx.demo.master_slave.context;

/**
 * <p>
 * 描述:
 * </p>
 * <p>
 * 版权所有: 版权所有(C)2001-2019
 * </p>
 * <p>
 * </p>
 * <p>
 * 版本1.0: 2019/10/17 15:16 新建
 * </p>
 *
 * @author 
 * @version 1.0
 */
public class DataSourceContextHolder {

    /**
     * 配置使用本地线程变量ThreadLocal,为每一个线程数据源类型保存自己单独的副本
     */
    private static final ThreadLocal<String> dataSource = new ThreadLocal<>();

    /**
     * 设置当前线程的数据源类型
     * @param dataSourceType
     */
    public static void setDataSourceType(String dataSourceType){
        dataSource.set(dataSourceType);
    }

    /**
     *   获取当前线程的数据源类型
     * @return
     */
    public static String getDataSourceType() {
        return dataSource.get();
    }

    /**
     * 移除本地线程变量
     */
    public static void clearDataSourceType() {
        dataSource.remove();
    }
}

此类主要是保存某个线程中数据源上下文的类,考虑到线程安全问题使用ThreadLocal类包装数据源类型;

ThreadLocal类是jdk提供的一个类,他主要是为每个线程变量保存了自己的一个副本,本质是一个Map结构,key是当前线程,value是线程变量,并发的情况下可以保证线程安全。
5.切面类

package com.xxx.demo.master_slave.aspect;

import com.xxx.demo.master_slave.annotation.DataSource;
import com.xxx.demo.master_slave.context.DataSourceContextHolder;
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.stereotype.Component;

import java.lang.reflect.Method;

/**
 * <p>
 * 描述:
 * </p>
 * <p>
 * 版权所有: 版权所有(C)2001-2019
 * </p>
 * <p>

 * </p>
 * <p>
 * 版本1.0: 2019/10/17 14:48 新建
 * </p>
 *
 * @author 
 * @version 1.0
 */
@Aspect
@Component
public class DataSourceAspect {

    private static final Logger logger = LoggerFactory.getLogger(DataSourceAspect.class);

    //切入点:所有使用了com.xxx.demo.master_slave.annotation.DataSource注解的方法
    @Pointcut("@annotation(com.xxx.demo.master_slave.annotation.DataSource)")
    public void dynamicDataSource(){

    }

    //切入方法前置调用
    @Before("dynamicDataSource()")
    public void before(JoinPoint joinPoint){
        MethodSignature signature = (MethodSignature)joinPoint.getSignature();
        Method method = signature.getMethod();
        logger.info("调用方法:{}",method.getName());
        DataSource dataSource = method.getAnnotation(DataSource.class);
        if(dataSource != null){
            DataSourceContextHolder.setDataSourceType(dataSource.value());
        }
    }

    //切入方法后置调用
    @After("dynamicDataSource()")
    public void after(){
        DataSourceContextHolder.clearDataSourceType();
    }
}


/**
* 切面注解类
*/
package com.xxx.demo.master_slave.annotation;

import java.lang.annotation.*;

/**
 * <p>
 * 描述:
 * </p>
 * <p>
 * 版权所有: 版权所有(C)2001-2019
 * </p>
 * <p>
 * </p>
 * <p>
 * 版本1.0: 2019/10/17 14:47 新建
 * </p>
 *
 * @author Xu yicheng
 * @version 1.0
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface DataSource {
    /**
     * 切换主从库 master/slave0
     * @return
     */
    String value();
}

主要逻辑是使用了DataSource注解的方法作为切入点,在加强的前置的调用中设置当前线程的数据源类型master/slave,master/slave由DataSource的value方法指定。

逻辑完成,下面看下测试效果

TestController.java

package com.xxx.demo.controller;

import com.xxx.demo.dao.entity.AccountBalance;
import com.xxx.demo.master_slave.annotation.DataSource;
import com.xxx.demo.scheduled.DyamicTask;
import com.xxx.demo.service.TestService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;

/**
 * <p>
 * 描述:
 * </p>
 * <p>
 * 版权所有: 版权所有(C)2001-2019
 * </p>
 * <p>
 * </p>
 * <p>
 * 版本1.0: 2019/9/24 20:20 新建
 * </p>
 *
 * @version 1.0
 */
@RequestMapping
@RestController
public class TestController {

    @Autowired
    private TestService testService;

    @ResponseBody
    @RequestMapping("/testQuery")
    @DataSource("slave")
    public String testQuery(){
        List<AccountBalance> list =  testService.queryList();
        if(list ==null){
            list = new ArrayList<>();
        }
        return Arrays.toString(list.toArray());
    }
    @ResponseBody
    @RequestMapping("/testInsert")
    @DataSource("master")
    public String testInsert(){
        AccountBalance accountBalance = new AccountBalance();
        accountBalance.setCustomerId(1001);
        accountBalance.setCustomerNm("master");
        accountBalance.setBalance(new BigDecimal(1.00));
        accountBalance.setAddDate(new Date());
        testService.insert(accountBalance);
        return "hello world";
    }

}

普通的controller类,唯一的不同点是在testQuery()方法上使用了DataSource注解,并指定slave和在testInsert()方法上使用了DataSource注解,并指定master;

TestService.java

package com.xxx.demo.service;

import com.xxx.demo.dao.TestDao;
import com.xxx.demo.dao.entity.AccountBalance;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * <p>
 * 描述:
 * </p>
 * <p>
 * 版权所有: 版权所有(C)2001-2019
 * </p>
 * <p>
 * </p>
 * <p>
 * 版本1.0: 2019/10/17 16:42 新建
 * </p>
 *
 * @author 
 * @version 1.0
 */
@Service
public class TestService {

    @Autowired
    private TestDao dao;

    public List<AccountBalance> queryList(){
        return dao.queryList();
    }

    public void insert(AccountBalance accountBalance){
        dao.insert(accountBalance);
    }
}

普通的service类;

TestDao.java

package com.xxx.demo.dao;

import com.xxx.demo.dao.entity.AccountBalance;
import org.apache.ibatis.annotations.*;
import org.springframework.stereotype.Repository;

import java.util.List;

/**
 * <p>
 * 描述:
 * </p>
 * <p>
 * 版权所有: 版权所有(C)2001-2019
 * </p>
 * <p>
 * </p>
 * <p>
 * 版本1.0: 2019/10/17 16:44 新建
 * </p>
 *
 * @version 1.0
 */
@Mapper
@Repository
public interface TestDao {

    @Select("select * from account_balance")
    @Results({
            @Result(column = "id", property = "id"),
            @Result(column = "customerId", property = "customerId"),
            @Result(column = "customernm", property = "customerNm"),
            @Result(column = "balance", property = "balance"),
            @Result(column = "adddate", property = "addDate")
    })
    List<AccountBalance> queryList();

    @Insert("insert into account_balance(customerId,customernm,balance,adddate) values (#{accountBalance.customerId},#{accountBalance.customerNm},#{accountBalance.balance},#{accountBalance.addDate})")
    void insert(@Param("accountBalance") AccountBalance accountBalance);
}

普通的mybatis 的mapper类;

测试:
http://localhost:8080/testInsert http://localhost:8080/testQuery

亲测可行!