美文网首页
Spring+Mybatis实现Mysql读写分离

Spring+Mybatis实现Mysql读写分离

作者: 倦鸟余花lee | 来源:发表于2018-11-25 17:36 被阅读0次

    前言

    随着系统中用户访问量的不断增加,数据库频繁访问将成为我们系统性能的瓶颈之一。经过对用户访问的分析,发现用户操作的特点是读多写少,而读操作通常耗时比较长,占用CPU较多。因为我们决定基于Spring AOP思想实现数据源的动态切换,进行数据库的读写分离。以下为相关逻辑以及伪代码。


    1. 创建数据源注解,用于service层区分读/写数据源
      DataSource.java
    package com.xdja.dataexchange.common.datasource;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**
     * @author lee
     * @description: 数据源DataSource注解
     * @date 2018-10-31 下午5:21:32
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface DataSource {
        String value();
    }
    
    1. 动态数据源切换,根据DataSource注解对当前线程动态切换读/写数据源。
      HandleDataSource.java
    package com.xdja.dataexchange.common.datasource;
    
    /**
     * @author lee
     * @description: 动态切换数据源
     * @date 2018-10-31 下午6:21:11
     */
    public class HandleDataSource {
    
        public static final ThreadLocal<String> holder = new ThreadLocal<String>();
    
        /**
         * 绑定当前线程数据源
         * @param datasource
         */
        public static void putDataSource(String datasource) {
            holder.set(datasource);
        }
    
        /**
         * 获取当前线程的数据源
         * @return
         */
        public static String getDataSource() {
            return holder.get();
        }
    }
    
    1. 使用Spring的 AbstractRoutingDataSource 获取目标数据源
      DynamicDataSource.java
    package com.xdja.dataexchange.common.datasource;
    
    import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
    
    /**
     * @author lee
     * @description: 获取目标数据源
     * @date 2018-10-31 下午6:20:16
     */
    public class DynamicDataSource extends AbstractRoutingDataSource {
    
        /**
         * 获取与数据源相关的key 此key是Map<String,DataSource> resolvedDataSources 中与数据源绑定的key值
         * 在通过determineTargetDataSource获取目标数据源时使用
         */
        @Override
        protected Object determineCurrentLookupKey() {
            return HandleDataSource.getDataSource();
        }
    }
    
    1. 创建数据库切面,在dao层方法获取datasource对象之前,在切面中指定当前线程数据源
      DataSourceAspect.java
    package com.xdja.dataexchange.aop;
    
    import com.xdja.dataexchange.common.datasource.DataSource;
    import com.xdja.dataexchange.common.datasource.HandleDataSource;
    import com.xdja.dataexchange.common.logger.LoggerUtil;
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.reflect.MethodSignature;
    
    import java.lang.reflect.Method;
    
    /**
     * @author lee
     * @description: 数据库切面
     * @date 2018-10-31 下午6:24:33
     */
    public class DataSourceAspect {
        /**
         * 在dao层方法获取datasource对象之前,在切面中指定当前线程数据源
         */
        public void before(JoinPoint point) {
    
            Object target = point.getTarget();
            String method = point.getSignature().getName();
    
            // 获取目标类的接口, 所以@DataSource需要写在接口上
            Class<?>[] classz = target.getClass().getInterfaces();
            Class<?>[] parameterTypes = ((MethodSignature) point.getSignature())
                    .getMethod().getParameterTypes();
    
            try {
                Method m = classz[0].getMethod(method, parameterTypes);
                if (m != null && m.isAnnotationPresent(DataSource.class)) {
                    DataSource data = m.getAnnotation(DataSource.class);
    
                    // 数据源放到当前线程中
                    HandleDataSource.putDataSource(data.value());
                }
            } catch (Exception e) {
                LoggerUtil.error("service error", e);
            }
        }
    }
    
    1. Spring配置Mybatis相关信息
      applicationContext-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:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.1.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.1.xsd"
           default-lazy-init="false">
        
        <!-- 配置写数据库 -->
        <bean id="writeDataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
            <property name="driverClass" value="${write.jdbc.driver}"/>
            <property name="jdbcUrl" value="${write.jdbc.url}"/>
            <property name="user" value="${write.jdbc.username}"/>
            <property name="password" value="${write.jdbc.password}"/>
            <!-- 连接关闭时默认将所有未提交的操作回滚。默认为false -->
            <property name="autoCommitOnClose" value="${write.autoCommitOnClose}"/>
            <!-- 连接池中保留的最小连接数-->
            <property name="minPoolSize" value="${write.minPoolSize}"/>
            <!-- 连接池中保留的最大连接数。默认为15 -->
            <property name="maxPoolSize" value="${write.maxPoolSize}"/>
            <!-- 初始化时获取的连接数,取值应在minPoolSize与maxPoolSize之间。默认为3 -->
            <property name="initialPoolSize" value="${write.initialPoolSize}"/>
            <!-- 最大空闲时间,超过空闲时间的连接将被丢弃。为0或负数则永不丢弃。默认为0秒 -->
            <property name="maxIdleTime" value="${write.maxIdleTime}"/>
            <!-- 当连接池中的连接用完时,C3P0一次性创建新连接的数目。默认为3 -->
            <property name="acquireIncrement" value="${write.acquireIncrement}"/>
            <!-- 定义在从数据库获取新连接失败后重复尝试获取的次数,默认为30 -->
            <property name="acquireRetryAttempts" value="${write.acquireRetryAttempts}"/>
            <!-- 当连接池用完时客户端调用getConnection()后等待获取新连接的时间,超时后将抛出SQLException,如设为0则无限期等待。单位毫秒,默认为0 -->
            <property name="checkoutTimeout" value="${write.checkoutTimeout}"/>
            <!--C3P0是异步操作的,缓慢的JDBC操作通过帮助进程完成。扩展这些操作可以有效的提升性能,通过多线程实现多个操作同时被执行。默认为3; -->
            <property name="numHelperThreads" value="${write.numHelperThreads}"/>
        </bean>
        
        <!-- 配置读数据库 -->
        <bean id="readDataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
            <property name="driverClass" value="${read.jdbc.driver}"/>
            <property name="jdbcUrl" value="${read.jdbc.url}"/>
            <property name="user" value="${read.jdbc.username}"/>
            <property name="password" value="${read.jdbc.password}"/>
            <!-- 连接关闭时默认将所有未提交的操作回滚。默认为false -->
            <property name="autoCommitOnClose" value="${read.autoCommitOnClose}"/>
            <!-- 连接池中保留的最小连接数-->
            <property name="minPoolSize" value="${read.minPoolSize}"/>
            <!-- 连接池中保留的最大连接数。默认为15 -->
            <property name="maxPoolSize" value="${read.maxPoolSize}"/>
            <!-- 初始化时获取的连接数,取值应在minPoolSize与maxPoolSize之间。默认为3 -->
            <property name="initialPoolSize" value="${read.initialPoolSize}"/>
            <!-- 最大空闲时间,超过空闲时间的连接将被丢弃。为0或负数则永不丢弃。默认为0秒 -->
            <property name="maxIdleTime" value="${read.maxIdleTime}"/>
            <!-- 当连接池中的连接用完时,C3P0一次性创建新连接的数目。默认为3 -->
            <property name="acquireIncrement" value="${read.acquireIncrement}"/>
            <!-- 定义在从数据库获取新连接失败后重复尝试获取的次数,默认为30 -->
            <property name="acquireRetryAttempts" value="${read.acquireRetryAttempts}"/>
            <!-- 当连接池用完时客户端调用getConnection()后等待获取新连接的时间,超时后将抛出SQLException,如设为0则无限期等待。单位毫秒,默认为0 -->
            <property name="checkoutTimeout" value="${read.checkoutTimeout}"/>
            <!--C3P0是异步操作的,缓慢的JDBC操作通过帮助进程完成。扩展这些操作可以有效的提升性能,通过多线程实现多个操作同时被执行。默认为3; -->
            <property name="numHelperThreads" value="${read.numHelperThreads}"/>
        </bean>
        
        <!-- 动态数据源,根据service接口上的注解来决定取哪个数据源 -->
        <bean id="dataSource" class="com.xdja.dataexchange.common.datasource.DynamicDataSource">
            <property name="targetDataSources">      
              <map key-type="java.lang.String">         
                 <entry key="write" value-ref="writeDataSource"/>      
                 <entry key="read" value-ref="readDataSource"/>      
              </map>               
            </property>   
            <property name="defaultTargetDataSource" ref="writeDataSource"/>      
        </bean>
    
        <!-- 设置sessionFactory -->
        <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
            <property name="dataSource" ref="dataSource"/>
            <!-- 自动扫描mapping.xml文件 -->
            <property name="mapperLocations" value="classpath:mapping/*.xml"/>
            <property name="typeAliasesPackage" value="com.xdja.dataexchange.pojo,com.xdja.dataexchange.dto"/>
        </bean>
    
        <!-- 配置扫描器,DAO接口所在包名,Spring会自动查找其下的类 -->
        <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
            <property name="basePackage" value="com.xdja.dataexchange.dao"/>
            <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
        </bean>
    
        <!-- (事务管理)transaction manager, use JtaTransactionManager for global tx -->
        <bean id="transactionManager"
              class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
            <property name="dataSource" ref="dataSource"/>
        </bean>
        
        <!-- 声明式开启 -->
        <tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true" order="1"/>
        
        <!-- 为业务逻辑层的方法解析@DataSource注解  为当前线程的HandlerDataSource注入数据源 -->
        <bean id="dataSourceAspect" class="com.xdja.dataexchange.aop.DataSourceAspect"/>
        <aop:config proxy-target-class="true">
            <aop:aspect id="dataSourceAspect" ref="dataSourceAspect" order="2">
                <aop:pointcut expression="execution(* com.xdja.dataexchange.service.impl..*.*(..))" id="pointcut"/>
                <aop:before pointcut-ref="pointcut" method="before"/>
            </aop:aspect>
        </aop:config>
    </beans>
    

    以上为数据库读写分离的定义部分,这里只简述了关键的流程,具体的配置文件加载以及项目启动在上述中未作说明。


    下面我们将具体介绍在service中使用读/写数据源的流程。

    1. 接口中读/写数据源的配置,如果接口需要使用读数据源,请在接口上方添加注解 @DataSource("readDataSource");如果接口需要使用写数据源,请在接口上方添加注解 @DataSource("writeDataSource")
      SyncObjectService.java
    package com.xdja.dataexchange.service;
    
    import com.xdja.dataexchange.common.datasource.DataSource;
    import com.xdja.dataexchange.pojo.*;
    
    import java.util.List;
    import java.util.Map;
    
    /**
     * @author lee
     * @description: 同步对象serivce
     * @date 2018-11-1 下午7:11:33
     */
    public interface SyncObjectService {
    
        /**
         * 保存同步对象
         *
         * @param logIndex
         * @param obj
         * @return
         */
        @DataSource("writeDataSource")
        public boolean saveDbSyncObj(long logIndex, SyncDatabaseObject obj);
        
        /**
         * 查询同步对象
         *
         * @param logIndex
         * @param id
         * @return
         */
        @DataSource("readDataSource")
        public SyncFileObject queryDbSyncObj(long logIndex, long id);
    }
    
    1. serviceImpl实现层逻辑,可根据具体业务进行事务管理,用法为 @Transactional(rollbackFor = Exception.class) ,其中 Exception.class 可为自定义异常类
      SyncObjectServiceImpl.java
    package com.xdja.dataexchange.service.impl;
    
    import com.xdja.dataexchange.common.logger.LoggerUtil;
    import com.xdja.dataexchange.dao.SyncObjectDao;
    import com.xdja.dataexchange.exception.ServiceException;
    import com.xdja.dataexchange.pojo.*;
    import com.xdja.dataexchange.service.SyncObjectService;
    import com.xdja.dataexchange.utils.Utils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Map;
    
    /**
     * @author lee
     * @description: 同步对象impl
     * @date 2018-11-2 上午9:09:01
     */
    @Service
    public class SyncObjectServiceImpl implements SyncObjectService {
    
        private final SyncObjectDao dao;
    
        @Autowired
        public SyncObjectServiceImpl(SyncObjectDao dao) {
            this.dao = dao;
        }
    
        @Override
        @Transactional(rollbackFor = Exception.class)
        public boolean saveDbSyncObj(long logIndex, SyncDatabaseObject obj) {
            try {
                // 相关保存逻辑
                // ...
            } catch (Exception e) {
                LoggerUtil.warn("save sync object happened exception, detail:{}", Utils.getStackTrace(e));
                throw new ServiceException("failed to save db sync object");
            }
            return true;
        }
    
        @Override
        public SyncDatabaseObject queryDbSyncObj(long logIndex, long id) {
            return dao.queryDbSyncObj(id);
        }
    }
    

    以上为项目中使用的Spring+Mybatis实现Mysql读写分离的方案,仅供参考。


    参考资料:
    https://www.cnblogs.com/lidj/p/7337535.html
    https://www.cnblogs.com/jeffen/p/6274038.html

    相关文章

      网友评论

          本文标题:Spring+Mybatis实现Mysql读写分离

          本文链接:https://www.haomeiwen.com/subject/ugvpqqtx.html