当前位置:Gxlcms > 数据库问题 > Spring+MyBatis实现数据库读写分离方案

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

时间:2021-07-01 10:21:17 帮助过:17人阅读

方案一:

通过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实现数据库读写分离方案

标签:切面   document   使用配置   username   XML   string   www   切换   ==   

人气教程排行