Spring + mybatis 主从数据库分离读写的几种方式(一)

Spring+mybatis 主从数据库分离读写(一)

——动态切换数据源方式

我们通过Spring AOP在业务层实现读写分离,也就是动态数据源的切换。在DAO层调用前定义切面,利用Spring的AbstractRoutingDataSource来解决多数据源的问题,用以实现动态选择数据源。我们可以通过注解实现自由切换DAO层接口指向的数据源。这样就使得代码变得极易扩展与便于阅读

步骤1、添加数据源至Spring配置文件中(必选)
添加对应数据源的URL

jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://192.168.12.244:3308/test?useUnicode=true&CharsetEncode=GBK&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true
#characterEncoding=GBK
jdbc.username=root
jdbc.password=1101399

jdbc.slave.driverClassName=com.mysql.jdbc.Driver
jdbc.slave.url=jdbc:mysql://192.168.12.244:3310/test?useUnicode=true&CharsetEncode=GBK&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true
#characterEncoding=GBK
jdbc.slave.username=SLAVE
jdbc.slave.password=SLAVE
<bean id="masterDataSource" class="org.apache.commons.dbcp.BasicDataSource"
       destroy-method="close">
       
          <property name="driverClassName" value="${jdbc.driverClassName}"/>
          <property name="url" value="${jdbc.url}"/>
          <property name="username" value="${jdbc.username}"/>
          <property name="password" value="${jdbc.password}"/>
          <property name="validationQuery" value="select 1"/>
       </bean>
       <bean id="slaveDataSources" class="org.apache.commons.dbcp.BasicDataSource"
       destroy-method="close">
       
          <property name="driverClassName" value="${jdbc.slave.driverClassName}"/>
          <property name="url" value="${jdbc.slave.url}"/>
          <property name="username" value="${jdbc.slave.username}"/>
          <property name="password" value="${jdbc.slave.password}"/>
          <property name="validationQuery" value="select 1"/>
       </bean>
<bean id="dataSource" class="com.zyh.domain.base.DynamicDataSource">  
        <property name="targetDataSources">
           <map key-type="java.lang.String">
                <entry value-ref="masterDataSource" key="MASTER"></entry>
                <entry value-ref="slaveDataSources" key="SLAVE"></entry>
            </map>
        </property>
        <!-- 新增:动态切换数据源         默认数据库 -->
        <property name="defaultTargetDataSource" ref="dataSource_m"></property>
    </bean> 

步骤2、定义一份枚举类型(可选)

package com.zyh.domain.base;

/**
 * 数据库对象枚举
 *
 * @author 1101399
 * @CreateDate 2018-6-20 上午9:27:49
 */
public enum DataSourceType {

    MASTER, SLAVE
}

步骤3、定义注解(必选)

我们使用注解是可以选择使用枚举类型,也可以选择直接使用数据源对应的key键值

package com.zyh.domain.base;

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

/**
 * 自定义注解,处理切换数据源
 *
 * @author 1101399
 * @CreateDate 2018-6-19 下午4:06:09
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
    /**
     * 注入映射注解:使用枚举类型应对配置文件数据库key键值
     */
    DataSourceType value();
    /**
     * 注入映射注解:直接键入配置文件中的key键值
     */
    String description() default "MASTER";
}

步骤4、数据源上下文配置(可选、推荐使用)

package com.zyh.domain.base;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;

/**
 * 根据数据源上下文进行判断,选择 方便进行通过注解进行数据源切换
 *
 * @author 1101399
 * @CreateDate 2018-6-19 下午3:59:44
 */
public class DataSourceContextHolder {

    /**
     * 控制台日志打印
     */
    private static final Logger log = LoggerFactory.getLogger(DataSourceContextHolder.class);
    /**
     * 线程本地环境
     */
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>() {
        @Override
        protected String initialValue() {
            return DataSourceType.MASTER.name();
        }
    };
    private static final ThreadLocal<DataSourceType> contextTypeHolder = new ThreadLocal<DataSourceType>() {
        /**
         * TODO 这个算是实现的关键
         *
         * 返回此线程局部变量的当前线程的初始值。最多在每次访问线程来获得每个线程局部变量时调用此方法一次,即线程第一次使用 get()
         * 方法访问变量的时候。如果线程先于 get 方法调用 set(T) 方法,则不会在线程中再调用 initialValue 方法。
         * 该实现只返回 null;如果程序员希望将线程局部变量初始化为 null 以外的某个值,则必须为 ThreadLocal
         * 创建子类,并重写此方法。通常,将使用匿名内部类。initialValue 的典型实现将调用一个适当的构造方法,并返回新构造的对象。
         *
         * 返回: 返回此线程局部变量的初始值
         */
        @Override
        protected DataSourceType initialValue() {
            return DataSourceType.MASTER;
        }
    };

    /**
     * 设置数据源类型:直接式
     *
     * @param dbType
     */
    public static void setDbType(String dbType) {
        Assert.notNull(dbType, "DataSourceType cannot be null");
        /**
         * 将此线程局部变量的当前线程副本中的值设置为指定值。许多应用程序不需要这项功能,它们只依赖于 initialValue()
         * 方法来设置线程局部变量的值。 参数: value - 存储在此线程局部变量的当前线程副本中的值。
         */
        contextHolder.set(dbType);
    }

    /**
     * 设置数据源类型:枚举式
     *
     * @param dbType
     */
    public static void setDataSourceType(DataSourceType dbType) {
        Assert.notNull(dbType, "DataSourceType cannot be null");
        /**
         * 将此线程局部变量的当前线程副本中的值设置为指定值。许多应用程序不需要这项功能,它们只依赖于 initialValue()
         * 方法来设置线程局部变量的值。 参数: value - 存储在此线程局部变量的当前线程副本中的值。
         */
        contextTypeHolder.set(dbType);
    }

    /**
     * 获取数据源类型:直接式
     *
     * @return
     */
    public static String getDbType() {
        /**
         * 返回此线程局部变量的当前线程副本中的值。如果这是线程第一次调用该方法,则创建并初始化此副本。 返回: 此线程局部变量的当前线程的值
         */
        return contextHolder.get();
    }

    /**
     * 获取数据源类型:枚举式
     *
     * @return
     */
    public static DataSourceType getDataSourceType() {
        return contextTypeHolder.get();
    }

    /**
     * 清楚数据类型
     */
    // 这个方法必不可少 否则切换数据库的时候有缓存现在
    public static void clearDbType() {
        /**
         * 移除此线程局部变量的值。这可能有助于减少线程局部变量的存储需求。如果再次访问此线程局部变量,那么在默认情况下它将拥有其
         * initialValue。
         */
        contextHolder.remove();
    }

    /**
     * 清除数据源类型
     */
    public static void clearDataSourceType() {
        /**
         * 移除此线程局部变量的值。这可能有助于减少线程局部变量的存储需求。如果再次访问此线程局部变量,那么在默认情况下它将拥有其
         * initialValue。
         */
        contextTypeHolder.remove();
    }

}

步骤5、数据源切换(必选)

package com.zyh.domain.base;

import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * 动态数据源
 *
 * @author 1101399
 * @CreateDate 2018-6-19 下午3:28:09
 */
public class DynamicDataSource extends AbstractRoutingDataSource {

    /**
     * 返回数据源key值 TODO 数据源切换关键部分
     */
    @Override
    protected Object determineCurrentLookupKey() {
        boolean testSwith = false;// 如果注解输入字符串-true、枚举-false
        if(testSwith){
            return DataSourceContextHolder.getDbType();
        }else{
            return DataSourceContextHolder.getDataSourceType().name();
        }
    }
}

在这里值的注意的是如果注解输入的类型是枚举类型的话

return DataSourceContextHolder.getDataSourceType();

不会实现数据源的切换(我想这也是使用该种方式进行数据库切换常见问题吧——至少我是看见好多地方都没有说到这一点),我们可以查看源代码

    /**
     * Retrieve the current target DataSource. Determines the
     * {@link #determineCurrentLookupKey() current lookup key}, performs
     * a lookup in the {@link #setTargetDataSources targetDataSources} map,
     * falls back to the specified
     * {@link #setDefaultTargetDataSource default target DataSource} if necessary.
     * @see #determineCurrentLookupKey()
     */
    protected DataSource determineTargetDataSource() {
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
        Object lookupKey = determineCurrentLookupKey();
        DataSource dataSource = this.resolvedDataSources.get(lookupKey);
        if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
            dataSource = this.resolvedDefaultDataSource;
        }
        if (dataSource == null) {
            throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
        }
        return dataSource;
    }

Object lookupKey = determineCurrentLookupKey();

DataSource dataSource = this.resolvedDataSources.get(lookupKey);

这两句代码是通过输入的数据库对应的键值实现切换数据源操作的,所以determineCurrentLookupKey()返回的最好是String类型的数据,说以我们如果使用枚举类型的注解信息输入我们最好是使用

return DataSourceContextHolder.getDataSourceType().name();

信息返回。

步骤6、Spring AOP 配置(必选)

Spring配置文件添加AOP切点设置

<aop:aspectj-autoproxy proxy-target-class="true" />
    <bean id="manyDataSourceAspect" class="com.zyh.domain.base.DataSourceAspect" />
    <aop:config>
        <aop:aspect id="dataSourceCutPoint" ref="manyDataSourceAspect">
         <aop:pointcut expression="execution(* com.zyh.dao.*.*.*(..))"
                id="dataSourceCutPoint" /><!-- 配置切点 -->
            <aop:before pointcut-ref="dataSourceCutPoint" method="before" />
            <aop:after pointcut-ref="dataSourceCutPoint" method="after" />
        </aop:aspect>
    </aop:config>

单独一句,切点配置一定要仔细。而我像一个250一样切点处少了一个层级活活找了2天时间,唉头发都掉了一大把、我还是个孩子啊。(# ̄~ ̄#)

package com.zyh.domain.base;

import java.lang.reflect.Method;
import java.sql.Connection;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.datasource.DataSourceUtils;

/**
 * 为AOP切面编程服务 为数据库动态切换配置 通过Spring配置AOP编程 Spring配置自动拦截相关操作 配合注解
 *
 * @author 1101399
 * @CreateDate 2018-6-20 上午9:47:43
 */
public class DataSourceAspect extends DataSourceUtils{

    /**
     * 配置控制台日志打印
     */
    private static final Logger LOG = LoggerFactory.getLogger(DataSourceAspect.class);

    /**
     * AOP切点对应的相关编程
     *
     * Aspect:关注点的模块化。类似于类声明,包含PointCut和对应的Advice。在Spring
     * AOP中被定义为接口@Aspect,作用于TYPE(类、接口、方法、enum)
     *
     * JoinPoint:程序执行过程中明确的点,如方法的调用或特定的异常被抛出。
     * 常用的是getArgs()用来获取参数,getTarget()获得目标对象。
     *
     * 相关资料:https://www.cnblogs.com/sjlian/p/7325602.html
     *
     * @param point
     * @throws Throwable
     */
    // ProceedingJoinPoint is only supported for around advice
    // ProceedingJoinPoint仅在around通知中受支持
    public void before(JoinPoint point){
        Object target = point.getTarget();
        String method = point.getSignature().getName();
        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)) {
                // 访问mapper中的注解
                DataSource data = m.getAnnotation(DataSource.class);// 获得注解对象
                switch (data.value()) {
                case MASTER:
                    DataSourceContextHolder.setDataSourceType(DataSourceType.MASTER);
                    LOG.info("using dataSource:{}", DataSourceType.MASTER);
                    break;
                case SLAVE:
                    DataSourceContextHolder.setDataSourceType(DataSourceType.SLAVE);

                    LOG.info("using dataSource:{}", DataSourceType.SLAVE);
                    break;
                default:
                    break;
                }
            } else {
                ;
            }
        } catch (Exception e) {
            LOG.error("dataSource annotation error:{}", e.getMessage());
            // 若出现异常,手动设为主库
            DataSourceContextHolder.setDataSourceType(DataSourceType.MASTER);
        } finally {
        }
    }

    /**
     * AOP切口结束执行
     * @param point
     */
    public void after(JoinPoint point) {
        DataSourceContextHolder.clearDataSourceType();
    }
}

步骤7、使用注解实现数据源的切换(可选)

package com.zyh.dao.file;

import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

import com.zyh.dao.base.MybatisMapper;
import com.zyh.domain.base.DataSource;
import com.zyh.domain.base.DataSourceType;
import com.zyh.domain.file.TXTFile;


/**
 * TXT文件的数据库接口
 *
 * @author      1101399
 * @CreateDate  2018-4-13 上午8:44:50
 */
@Repository(value="file.TXTFileMapper")
public interface TXTFileMapper extends MybatisMapper<TXTFile,Integer>{

    TXTFile findByName(@Param("name") String name);

    @DataSource(DataSourceType.SLAVE)
    TXTFile findDataById(@Param("id") Integer id);

}

至于其他的数据源切换方式我们改日在谈(* ̄︶ ̄)

原文地址:https://www.cnblogs.com/supperlhg/p/9235126.html