zoukankan      html  css  js  c++  java
  • 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);
    
    }

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

  • 相关阅读:
    创建型模式(四) 单例模式
    创建型模式(三) 原型模式
    创建型模式(二) 建造者模式
    创建型模式(一) 简单工厂模式、工厂模式与抽象工厂模式
    Django15-分页功能
    Django14-Ajax删除按钮动态效果
    网络day04-配置备份、清除、密码恢复、IOS更新
    网络day03-NTP配置和SMTP配置
    网络day02-设备配置远程登录
    HTML注释
  • 原文地址:https://www.cnblogs.com/supperlhg/p/9235126.html
Copyright © 2011-2022 走看看