原创

springboot基于mybaits实现mysql读写分离

温馨提示:
本文最后更新于 2023年05月11日,已超过 344 天没有更新。若文章内的图片失效(无法正常加载),请留言反馈或直接联系我

配置mysql配置项

这个根据自己项目的配置项进行,有的习惯在mybaits下配置db,我的是在spring.datasource配置:
file

master名字,slaver1名字自己取,也可以叫write,read

配置mybaits配置项:


# MyBatis
mybatis:
  type-aliases-package: com.zyd.blog.persistence.beans
  mapper-locations: classpath:/mybatis/*.xml
  config-location: classpath:/config/mybatis-config.xml

配置数据源切换选择类

由于涉及到事务处理,可能会遇到事务中同时用到读库和写库,可能会有延时造成脏读,所以增加了线程变量设置,来保证一个事务内读写都是同一个库
新增文件

package com.zyd.blog.framework.holder;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 利用ThreadLocal封装的保存数据源上线的上下文context
 */
public class DataSourceContextHolder {
    private static Logger log = LoggerFactory.getLogger(DataSourceContextHolder.class);
    public static final String WRITE = "master";
    public static final String READ = "salver";

    private static ThreadLocal<String> contextHolder= new ThreadLocal<>();

    public static void setDbType(String dbType) {
        if (dbType == null) {
            log.error("dbType为空");
            throw new NullPointerException();
        }
        log.info("设置dbType为:{}",dbType);
        contextHolder.set(dbType);
    }

    public static String getDbType() {
        return contextHolder.get() == null ? WRITE : contextHolder.get();
    }

    public static void clearDbType() {
        contextHolder.remove();
    }
}

配置数据源路由类

新增文件:DataSourceRouter.java,继承AbstractRoutingDataSource ,重写determineCurrentLookupKey,用于动态切换数据源配置

package com.zyd.blog.framework.config;

import com.zyd.blog.framework.holder.DataSourceContextHolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import java.util.Random;

public class DataSourceRouter extends AbstractRoutingDataSource {
    @Value("${spring.datasource.num}")
    private int num;

    private final Logger log = LoggerFactory.getLogger(this.getClass());

    @Override
    protected Object determineCurrentLookupKey() {
        String typeKey = DataSourceContextHolder.getDbType();
        if (typeKey == DataSourceContextHolder.WRITE) {
//            System.out.println("使用了写库");
            log.info("使用了写库");
            return typeKey;
        }
        //随机1-num的随机数
        Random rand = new Random();
        int randomNumber = rand.nextInt(num) + 1;
        //使用随机数决定使用哪个读库
        log.info("使用了读库"+randomNumber);
//        System.out.println("使用了读库"+randomNumber);
        return DataSourceContextHolder.READ+randomNumber;
    }
}

配置数据源 (懒的人可以看到最后,直接复制java文件代码)

新增MybatisConfig.java,你也可以取别的名字,比如DatabaseConfig.java

首先我们需要获取到所有的数据库配置项

    /**
     * 写数据源
     *
     * @Primary 标志这个 Bean 如果在多个同类 Bean 候选时,该 Bean 优先被考虑。
     * 多数据源配置的时候注意,必须要有一个主数据源,用 @Primary 标志该 Bean
     */
    @Primary
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource writeDataSource() {
        return new DruidDataSource();
    }

    /**
     * 读数据源,有几个就配置几个
     */
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.slaver1")
    public DataSource read1() {
        return new DruidDataSource();
    }
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.slaver2")
    public DataSource read2() {
        return new DruidDataSource();
    }

配置config mapper注解和自定义sqlsession注解

@MapperScan(basePackages = "com.zyd.blog.persistence.mapper",sqlSessionFactoryRef = "sqlSessionFactory")

配置自定义SQLSessionFactory

由于多数据库连接,所以需要自定义sqlSessionFactory,指定数据库连接和实例化配置
在 mybaitsConfig.java额外增加属性:

    @Value("${mybatis.type-aliases-package}")
    private String typeAliasesPackage;

    @Value("${mybatis.mapper-locations}")
    private String mapperLocation;

    @Value("${mybatis.config-location}")
    private String configLocation;

自定义sqlsession工厂方法:

    /**
     * 多数据源需要自己设置sqlSessionFactory
     */
    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(routingDataSource());
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        // 实体类对应的位置
        bean.setTypeAliasesPackage(typeAliasesPackage);
        // mybatis的XML的配置
        bean.setMapperLocations(resolver.getResources(mapperLocation));
        bean.setConfigLocation(resolver.getResource(configLocation));
        return bean.getObject();
    }

设置事务

    /**
     * 设置事务,事务需要知道当前使用的是哪个数据源才能进行事务处理
     */
    @Bean
    public DataSourceTransactionManager dataSourceTransactionManager() {
        return new DataSourceTransactionManager(routingDataSource());
    }

完整文件:

package com.zyd.blog.framework.config;

import com.alibaba.druid.pool.DruidDataSource;
import com.zyd.blog.framework.holder.DataSourceContextHolder;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.annotation.Value;
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.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import tk.mybatis.spring.annotation.MapperScan;
import org.springframework.stereotype.Component;

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

/**
 * @author yadong.zhang (yadong.zhang0415(a)gmail.com)
 * @version 1.0
 * @website https://docs.zhyd.me
 * @date 2018/4/16 16:26
 * @since 1.0
 */
@Configuration
@MapperScan(basePackages = "com.zyd.blog.persistence.mapper", sqlSessionFactoryRef = "sqlSessionFactory")
public class MybatisConfig {
    @Value("${mybatis.type-aliases-package}")
    private String typeAliasesPackage;

    @Value("${mybatis.mapper-locations}")
    private String mapperLocation;

    @Value("${mybatis.config-location}")
    private String configLocation;

    /**
     * 写数据源
     *
     * @Primary 标志这个 Bean 如果在多个同类 Bean 候选时,该 Bean 优先被考虑。
     * 多数据源配置的时候注意,必须要有一个主数据源,用 @Primary 标志该 Bean
     */
    @Primary
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource master() {
        return new DruidDataSource();
    }

    /**
     * 读数据源,有几个就配置几个
     */
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.slaver1")
    public DataSource slaver1() {
        return new DruidDataSource();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.slaver2")
    public DataSource slaver2() {
        return new DruidDataSource();
    }




    /**
     * 多数据源需要自己设置sqlSessionFactory
     */
    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(routingDataSource());
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        // 实体类对应的位置
        bean.setTypeAliasesPackage(typeAliasesPackage);
        // mybatis的XML的配置
        bean.setMapperLocations(resolver.getResources(mapperLocation));
        bean.setConfigLocation(resolver.getResource(configLocation));
        return bean.getObject();
    }

    /**
     * 设置事务,事务需要知道当前使用的是哪个数据源才能进行事务处理
     */
    @Bean
    public DataSourceTransactionManager dataSourceTransactionManager() {
        return new DataSourceTransactionManager(routingDataSource());
    }


    /**
     * 设置数据源路由,通过该类中的determineCurrentLookupKey决定使用哪个数据源
     */
    @Bean
    public AbstractRoutingDataSource routingDataSource() {
        DataSourceRouter proxy = new DataSourceRouter();
        Map<Object, Object> targetDataSources = new HashMap<>(3);
        targetDataSources.put(DataSourceContextHolder.WRITE, master());
        targetDataSources.put(DataSourceContextHolder.READ + 1, slaver1());
        targetDataSources.put(DataSourceContextHolder.READ + 2, slaver2());
        proxy.setDefaultTargetDataSource(master());
        proxy.setTargetDataSources(targetDataSources);
        return proxy;
    }
}

在业务中区分读写

只需要调用 DataSourceContextHolder.setDbType(DataSourceContextHolder.READ); 即可切换读写

file

注解区分

在实际业务中,手动切换并不方便,我们可以使用java的AOP注解方式去实现注解切换,
新增DBRead.java文件

package com.zyd.blog.framework.mysql;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Description 通过该接口注释的service使用读模式,其他使用写模式
 *
 * 接口注释只是一种办法,如果项目已经有代码了,通过注释可以不修改任何业务代码加持读写分离
 * 也可以通过切面根据方法开头来设置读写模式,例如getXXX()使用读模式,其他使用写模式
 *
 * @author fxb
 * @date 2018-08-31
 */
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
    public @interface DBRead {
}

新增ReadInterceptor.java文件,用于配置切面

package com.zyd.blog.framework.mysql;


import com.zyd.blog.framework.holder.DataSourceContextHolder;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;

/**
 * Description
 *
 * @author fxb
 * @date 2018-08-31
 */
@Aspect
@Component
public class ReadInterceptor implements Ordered {
    private static final Logger log = LoggerFactory.getLogger(ReadInterceptor.class);

    @Around("@annotation(DBRead)")
    public Object setRead(ProceedingJoinPoint joinPoint, DBRead DBRead) throws Throwable {
        try {
            //如果当前线程已经是已读,则不做修改
            DataSourceContextHolder.setDbType(DataSourceContextHolder.READ);
            return joinPoint.proceed();
        } finally {
            DataSourceContextHolder.clearDbType();
            log.info("清除threadLocal");
        }
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

在方法上配置@DBRead注解即可:
file

完成
file

正文到此结束
本文目录