美文网首页
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