Spring+MyBatis实现数据库读写分离方案

Posted li150dan

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring+MyBatis实现数据库读写分离方案相关的知识,希望对你有一定的参考价值。

本文重点介绍两种方案实现读写分离,推荐第二种方案

方案一:

通过Spring AOP在Service业务层实现读写分离,在调用DAO数据层前定义切面,利用Spring的AbstractRoutingDataSource解决多数据源的问题,实现动态选择数据源
  • 优点:通过注解的方法在Service业务层(接口或者实现类)每个方法上配置数据源,原有代码改动量少,支持多读,易扩展
  • 缺点:需要在Service业务层(接口或者实现类)每个方法上配置注解,人工管理,容易出错

方案二:

如果后台结构是spring+mybatis,可以通过spring的AbstractRoutingDataSource和mybatis Plugin拦截器实现非常友好的读写分离,原有代码不需要任何改变
  • 优点:原有代码不变,支持多读,易扩展
  • 缺点:

下面就详细介绍这两种方案的具体实现,先贴上用Maven构建的SSM项目目录结构图:

技术图片

 

方案一实现方式介绍:

1. 定义注解

package com.demo.annotation;

import java.lang.annotation.*;

/**
 * 自定义注解
 * 动态选择数据源时使用
 */
@Documented
@Target(ElementType.METHOD) //可以应用于方法
@Retention(RetentionPolicy.RUNTIME) //标记的注释由JVM保留,因此运行时环境可以使用它
public @interface DataSourceChange 
    boolean slave() default false;

2. 定义类DynamicDataSourceHolder

package com.demo.datasource;

import lombok.extern.slf4j.Slf4j;

/**
 * @ProjectName: ssm-maven
 * @Package: com.demo.datasource
 * @ClassName: DynamicDataSourceHolder
 * @Description: 设置和获取动态数据源KEY
 * @Author: LiDan
 * @Date: 2019/7/10 16:15
 * @Version: 1.0
 */
@Slf4j
public class DynamicDataSourceHolder 
    /**
     * 线程安全,记录当前线程的数据源key
     */
    private static ThreadLocal<String> contextHolder = new ThreadLocal<String>();
    /**
     * 主库,只允许一个
     */
    public static final String DB_MASTER = "master";
    /**
     * 从库,允许多个
     */
    public static final String DB_SLAVE = "slave";

    /**
     * 获取当前线程的数据源
     * @return
     */
    public static String getDataSource() 
        String db = contextHolder.get();
        if(db == null) 
            //默认是master库
            db = DB_MASTER;
        
        log.info("所使用的数据源为:" + db);
        return db;
    

    /**
     * 设置当前线程的数据源
     * @param dataSource
     */
    public static void setDataSource(String dataSource) 
        contextHolder.set(dataSource);
    

    /**
     * 清理连接类型
     */
    public static void clearDataSource() 
        contextHolder.remove();
    

    /**
     * 判断是否是使用主库,提高部分使用
     * @return
     */
    public static boolean isMaster() 
        return DB_MASTER.equals(getDataSource());
    

3. 定义类DynamicDataSource继承自AbstractRoutingDataSource

技术图片
package com.demo.datasource;

import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.util.ReflectionUtils;

import javax.sql.DataSource;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @ProjectName: ssm-maven
 * @Package: com.demo.datasource
 * @ClassName: DynamicDataSource
 * @Description: 动态数据源实现读写分离
 * @Author: LiDan
 * @Date: 2019/7/10 16:28
 * @Version: 1.0
 */
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource 
    /**
     * 获取读数据源方式,0:随机,1:轮询
     */
    private int readDataSourcePollPattern = 0;

    /**
     * 读数据源个数
     */
    private int slaveCount = 0;

    /**
     * 记录读库的key
     */
    private List<Object> slaveDataSources = new ArrayList<Object>(0);

    /**
     * 轮询计数,初始为0,AtomicInteger是线程安全的
     */
    private AtomicInteger counter = new AtomicInteger(0);

    /**
     * 每次操作数据库都会调用此方法,根据返回值动态选择数据源
     * 定义当前使用的数据源(返回值为动态数据源的key值)
     * @return
     */
    @Override
    protected Object determineCurrentLookupKey() 
        //如果使用主库,则直接返回
        if (DynamicDataSourceHolder.isMaster()) 
            return DynamicDataSourceHolder.getDataSource();
        
        int index = 0;
        //如果不是主库则选择从库
        if(readDataSourcePollPattern == 1) 
            //轮询方式
            index = getSlaveIndex();
        
        else 
            //随机方式
            index = ThreadLocalRandom.current().nextInt(0, slaveCount);
        
        log.info("选择从库索引:"+index);
        return slaveDataSources.get(index);
    

    /**
     * 该方法会在Spring Bean 加载初始化的时候执行,功能和 bean 标签的属性 init-method 一样
     * 把所有的slave库key放到slaveDataSources里
     */
    @SuppressWarnings("unchecked")
    @Override
    public void afterPropertiesSet() 
        super.afterPropertiesSet();

        // 由于父类的resolvedDataSources属性是私有的子类获取不到,需要使用反射获取
        Field field = ReflectionUtils.findField(AbstractRoutingDataSource.class, "resolvedDataSources");
        // 设置可访问
        field.setAccessible(true);

        try 
            Map<Object, DataSource> resolvedDataSources = (Map<Object, DataSource>) field.get(this);
            // 读库的数据量等于数据源总数减去写库的数量
            this.slaveCount = resolvedDataSources.size() - 1;
            for (Map.Entry<Object, DataSource> entry : resolvedDataSources.entrySet()) 
                if (DynamicDataSourceHolder.DB_MASTER.equals(entry.getKey())) 
                    continue;
                
                slaveDataSources.add(entry.getKey());
            
         catch (Exception e) 
            e.printStackTrace();
        
    

    /**
     * 轮询算法实现
     * @return
     */
    private int getSlaveIndex() 
        long currValue = counter.incrementAndGet();
        if (counter.get() > 9999)  //以免超出int范围
            counter.set(0); //还原
        
        //得到的下标为:0、1、2、3……
        int index = (int)(currValue % slaveCount);
        return index;
    

    public void setReadDataSourcePollPattern(int readDataSourcePollPattern) 
        this.readDataSourcePollPattern = readDataSourcePollPattern;
    
View Code

4. 定义AOP切面类DynamicDataSourceAspect

技术图片
package com.demo.aop;

import com.demo.annotation.DataSourceChange;
import com.demo.datasource.DynamicDataSourceHolder;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;

import java.lang.reflect.Method;

/**
 * @ProjectName: ssm-maven
 * @Package: com.demo.aop
 * @ClassName: DynamicDataSourceAspect
 * @Description: 定义选择数据源切面
 * @Author: LiDan
 * @Date: 2019/7/11 11:05
 * @Version: 1.0
 */
@Slf4j
public class DynamicDataSourceAspect 
    /**
     * 目标方法执行前调用
     * @param point
     */
    public void before(JoinPoint point) 
        log.info("before");
        //获取代理接口或者类
        Object target = point.getTarget();
        String methodName = point.getSignature().getName();
        //获取目标类的接口,所以注解@DataSourceChange需要写在接口里面
        //Class<?>[] clazz = target.getClass().getInterfaces();
        //获取目标类,所以注解@DataSourceChange需要写在类里面
        Class<?>[] clazz = new Class<?>[]target.getClass();
        Class<?>[] parameterTypes = ((MethodSignature) point.getSignature()).getMethod().getParameterTypes();
        try 
            Method method = clazz[0].getMethod(methodName, parameterTypes);
            //判断方法上是否使用了该注解
            if (method != null && method.isAnnotationPresent(DataSourceChange.class)) 
                DataSourceChange data = method.getAnnotation(DataSourceChange.class);
                if (data.slave()) 
                    DynamicDataSourceHolder.setDataSource(DynamicDataSourceHolder.DB_SLAVE);
                 else 
                    DynamicDataSourceHolder.setDataSource(DynamicDataSourceHolder.DB_MASTER);
                
            
         catch (Exception ex) 
            log.error(String.format("Choose DataSource error, method:%s, msg:%s", methodName, ex.getMessage()));
        
    

    /**
     * 目标方法执行后调用
     * @param point
     */
    public void after(JoinPoint point) 
        log.info("after");
        DynamicDataSourceHolder.clearDataSource();
    

    /**
     * 环绕通知
     * @param joinPoint
     * @return
     */
    public Object around(ProceedingJoinPoint joinPoint) 
        log.info("around");
        Object result = null;
        //获取代理接口或者类
        Object target = joinPoint.getTarget();
        String methodName = joinPoint.getSignature().getName();
        //获取目标类的接口,所以注解@DataSourceChange需要写在接口上
        //Class<?>[] clazz = target.getClass().getInterfaces();
        //获取目标类,所以注解@DataSourceChange需要写在类里面
        Class<?>[] clazz = new Class<?>[]target.getClass();
        Class<?>[] parameterTypes = ((MethodSignature) joinPoint.getSignature()).getMethod().getParameterTypes();
        try 
            Method method = clazz[0].getMethod(methodName, parameterTypes);
            //判断方法上是否使用了该注解
            if (method != null && method.isAnnotationPresent(DataSourceChange.class)) 
                DataSourceChange data = method.getAnnotation(DataSourceChange.class);
                if (data.slave()) 
                    DynamicDataSourceHolder.setDataSource(DynamicDataSourceHolder.DB_SLAVE);
                 else 
                    DynamicDataSourceHolder.setDataSource(DynamicDataSourceHolder.DB_MASTER);
                
            

            System.out.println("--环绕通知开始--开启事务--自动--");
            long start = System.currentTimeMillis();

            //调用 proceed() 方法才会真正的执行实际被代理的目标方法
            result = joinPoint.proceed();

            long end = System.currentTimeMillis();
            System.out.println("总共执行时长" + (end - start) + " 毫秒");

            System.out.println("--环绕通知结束--提交事务--自动--");
        
        catch (Throwable ex) 
            System.out.println("--环绕通知--出现错误");
            log.error(String.format("Choose DataSource error, method:%s, msg:%s", methodName, ex.getMessage()));
        
        finally 
            DynamicDataSourceHolder.clearDataSource();
        
        return result;
    
View Code

5. 配置spring-mybatis.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                        http://www.springframework.org/schema/beans/spring-beans.xsd
                        http://www.springframework.org/schema/context
                        http://www.springframework.org/schema/context/spring-context.xsd
                        http://www.springframework.org/schema/tx
                        http://www.springframework.org/schema/tx/spring-tx.xsd">
    <!-- 自动扫描 -->
    <!--<context:component-scan base-package="com.demo.dao" />-->

    <!-- 引入配置文件 -->
    <context:property-placeholder location="classpath:properties/jdbc.properties"/>
    <!--<bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">-->
    <!--<property name="location" value="classpath:properties/jdbc.properties" />-->
    <!--</bean>-->

    <!-- DataSource数据库配置-->
    <bean id="abstractDataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="$jdbc.driver"/>
    </bean>
    <!-- 写库配置-->
    <bean id="dataSourceMaster" parent="abstractDataSource">
        <property name="driverClassName" value="$jdbc.driver"/>
        <property name="url" value="$jdbc.master.url"/>
        <property name="username" value="$jdbc.master.username"/>
        <property name="password" value="$jdbc.master.password"/>
    </bean>
    <!-- 从库一配置-->
    <bean id="dataSourceSlave1" parent="abstractDataSource">
        <property name="driverClassName" value="$jdbc.driver"/>
        <property name="url" value="$jdbc.slave.one.url"/>
        <property name="username" value="$jdbc.slave.one.username"/>
        <property name="password" value="$jdbc.slave.one.password"/>
    </bean>
    <!-- 从库二配置-->
    <bean id="dataSourceSlave2" parent="abstractDataSource">
        <property name="driverClassName" value="$jdbc.driver"/>
        <property name="url" value="$jdbc.slave.two.url"/>
        <property name="username" value="$jdbc.slave.two.username"/>
        <property name="password" value="$jdbc.slave.two.password"/>
    </bean>

    <!-- 设置自己定义的动态数据源 -->
    <bean id="dataSource" class="com.demo.datasource.DynamicDataSource">
        <!-- 设置动态切换的多个数据源 -->
        <property name="targetDataSources">
            <map>
                <!-- 这个key需要和程序中的key一致 -->
                <entry value-ref="dataSourceMaster" key="master"></entry>
                <entry value-ref="dataSourceSlave1" key="slave1"></entry>
                <entry value-ref="dataSourceSlave2" key="slave2"></entry>
            </map>
        </property>
        <!-- 设置默认的数据源,这里默认走写库 -->
        <property name="defaultTargetDataSource" ref="dataSourceMaster"/>
        <!-- 轮询方式 0:随机,1:轮询 -->
        <property name="readDataSourcePollPattern" value="1" />
    </bean>

    <!-- spring和MyBatis完美整合,不需要mybatis的配置映射文件 -->
    <!--<bean id="mysqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">-->
    <bean id="mySqlSessionFactory" class="com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean">
        <!--mybatis的配置文件-->
        <property name="configLocation" value="classpath:beans/mybatis-config.xml"/>
        <!-- 自动扫描sqlMapper下面所有xml文件 -->
        <property name="mapperLocations">
            <list>
                <value>classpath:sqlmapper/**/*.xml</value>
            </list>
        </property>
        <property name="dataSource" ref="dataSource"/>
        <property name="typeAliasesPackage" value="com.demo.model"/>
    </bean>

    <!-- DAO接口所在包名,Spring会自动查找其下的类 -->
    <bean id="daoMapper" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="sqlSessionFactoryBeanName" value="mySqlSessionFactory"></property>
        <property name="basePackage" value="com.demo.dao"/>
    </bean>

    <!-- JDBC事务管理器 -->
    <!--<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">-->
    <bean id="transactionManager" class="com.demo.datasource.DynamicDataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
        <property name="rollbackOnCommitFailure" value="true"/>
    </bean>

    <!-- 开启事务管理器的注解 -->
    <tx:annotation-driven transaction-manager="transactionManager" />

</beans>

6. 配置spring-aop.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                        http://www.springframework.org/schema/beans/spring-beans.xsd
                        http://www.springframework.org/schema/aop
                        http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 配置动态选择数据库全自动方式aop -->
    <!--定义切面类-->
    <bean id="dynamicDataSourceAspect" class="com.demo.aop.DynamicDataSourceAspect" />
    <aop:config>
        <!--定义切点,就是要监控哪些类下的方法-->
        <!--说明:该切点不能用于dao层,因为无法提前拦截到动态选择的数据源-->
        <aop:pointcut id="myPointCut" expression="execution(* com.demo.service..*.*(..))"/>
        <!--order表示切面顺序(多个切面时或者和JDBC事务管理器同时用时)-->
        <aop:aspect ref="dynamicDataSourceAspect" order="1">
            <aop:before method="before" pointcut-ref="myPointCut"/>
            <aop:after method="after" pointcut-ref="myPointCut"/>
            <!--<aop:around method="around" pointcut-ref="myPointCut"/>-->
        </aop:aspect>
    </aop:config>
    <!-- 配置动态选择数据库全自动方式aop -->

    <!--
           启动AspectJ支持,开启自动注解方式AOP
           使用配置注解,首先我们要将切面在spring上下文中声明成自动代理bean
           默认情况下会采用JDK的动态代理实现AOP(只能对实现了接口的类生成代理,而不能针对类)
           如果proxy-target-class="true" 声明时强制使用cglib代理(针对类实现代理)
    -->
    <!--<aop:aspectj-autoproxy proxy-target-class="true"/>-->
</beans>

注意在applicationContext.xml中导入这两个xml

    <!-- 导入mybatis配置文件 -->
    <import resource="classpath:beans/spring-mybatis.xml"></import>
    <!-- 导入spring-aop配置文件 -->
    <import resource="classpath:beans/spring-aop.xml"></import>

最后可以在Service业务层接口或者实现类具体方法上打DataSourceChange注解

注意:注解是写在接口方法上还是实现类方法上要根据前面步骤4定义aop切面时获取注解的方式定

package com.demo.serviceimpl;

import com.demo.annotation.DataSourceChange;
import com.demo.dao.CmmAgencyDao;
import com.demo.dao.CmmAgencystatusDao;
import com.demo.model.bo.TCmmAgencyBO;
import com.demo.model.bo.TCmmAgencystatusBO;
import com.demo.model.po.TCmmAgencyPO;
import com.demo.service.AgencyService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * @ProjectName: ssm-maven
 * @Package: com.demo.serviceimpl
 * @ClassName: AgencyServiceImpl
 * @Description: 业务逻辑实现层
 * @Author: LiDan
 * @Date: 2019/6/18 17:41
 * @Version: 1.0
 */
@Slf4j
@Service
public class AgencyServiceImpl implements AgencyService 
    @Autowired
    private CmmAgencyDao cmmAgencyDao;
    @Autowired
    private CmmAgencystatusDao cmmAgencystatusDao;

    /**
     * 查询信息
     * @param bussnum
     * @return
     */
    @Override
    @DataSourceChange(slave = true) //读库
    @Transactional(readOnly = true) //指定事务是否为只读取数据:只读
    public TCmmAgencyPO selectAgencyByBussNum(String bussnum) 
       
    

    /**
     * 修改信息
     * @param bussnum
     * @return
     */
    @Override
    @Transactional(rollbackFor = Exception.class) //声明式事务控制
    public boolean updateAgencyByBussNum(String bussnum) 
     
    

 

以上是关于Spring+MyBatis实现数据库读写分离方案的主要内容,如果未能解决你的问题,请参考以下文章

Spring+Mybatis实现主从数据库读写分离

Mybatis+Spring实现Mysql读写分离

Spring动态数据源+Mybatis拦截器实现数据库读写分离

Spring Boot+MyBatis实现读写分离

Spring + Mybatis项目实现数据库读写分离

Spring Boot + MyBatis + MySQL 实现读写分离