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


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

    其本质和Spring + mybatis 主从数据库分离读写的几种方式(一)中的数据源切换核心内容一致。但是与之也有不同之处:后者是用Spring AOP切面编程拦截判断注解的方式实现数据库的切换,而前者的实现则是依赖重写mybatis事务提交而实现的(org.springframework.jdbc.datasource.DataSourceTransactionManager),将指定的数据源操作进行拦截,并重新定义数据源指向来实现数据源的自动切换。

    我使用的是MyBatis 3.0

    这种方法的优点:可以对已经开发完毕的系统进行数据库主从读取分离(读取操作使用从库、写操作使用主库)

    步骤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、定义注解(必选。。。额 抱歉貌似在这种方法中这个不需要 ┓( ´∀` )┏)

    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、定义myBatis拦截器

    package com.zyh.domain.base;
    
    import java.util.Locale;
    import java.util.Map;
    import java.util.Properties;
    import java.util.concurrent.ConcurrentHashMap;
    
    import org.apache.ibatis.executor.Executor;
    import org.apache.ibatis.executor.keygen.SelectKeyGenerator;
    import org.apache.ibatis.mapping.BoundSql;
    import org.apache.ibatis.mapping.MappedStatement;
    import org.apache.ibatis.mapping.SqlCommandType;
    import org.apache.ibatis.plugin.Interceptor;
    import org.apache.ibatis.plugin.Intercepts;
    import org.apache.ibatis.plugin.Invocation;
    import org.apache.ibatis.plugin.Plugin;
    import org.apache.ibatis.plugin.Signature;
    import org.apache.ibatis.session.ResultHandler;
    import org.apache.ibatis.session.RowBounds;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.transaction.support.TransactionSynchronizationManager;
    
    /**
     * 自定义 myBatis 拦截器
     *
     * @author      1101399
     * @CreateDate  2018-6-29 下午4:55:31
     */
    @Intercepts({
            @Signature(type = Executor.class, method = "update", args = { MappedStatement.class,
                    Object.class }),
            @Signature(type = Executor.class, method = "query", args = { MappedStatement.class,
                    Object.class, RowBounds.class, ResultHandler.class }) })
    public class DynamicTransactionManagerPlugin implements Interceptor {
    
        private static final Logger log = LoggerFactory
                .getLogger(DynamicTransactionManagerPlugin.class);
        private static final String REGEX = ".*insert\u0020.*|.*delete\u0020.*|.*update\u0020.*";
        private static final Map<String, DataSourceType> cacheMap = new ConcurrentHashMap<>();
    
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            // TODO Auto-generated method stub
            log.info("DynamicTransactionManagerPlugin.intercept");
            boolean sysnchronizationActive = TransactionSynchronizationManager
                    .isSynchronizationActive();
            if (!sysnchronizationActive) {
                Object[] objects = invocation.getArgs();
                MappedStatement ms = (MappedStatement) objects[0];
    
                DataSourceType dataSourceType = null;
    
                if ((dataSourceType = cacheMap.get(ms.getId())) == null) {
                    // 读方法
                    log.info("DynamicTransactionManagerPlugin.intercept.ms.getSqlCommandType()|"
                            + ms.getSqlCommandType());
                    if (ms.getSqlCommandType().equals(SqlCommandType.SELECT)) {
                        // !selectKey 为自增id查询主键(SELECT LAST_INSERT_ID() )方法,使用主库
                        if (ms.getId().contains(SelectKeyGenerator.SELECT_KEY_SUFFIX)) {
                            dataSourceType = DataSourceType.SLAVE;
                        } else {
                            BoundSql boundSql = ms.getSqlSource().getBoundSql(objects[1]);
                            String sql = boundSql.getSql().toLowerCase(Locale.CHINA)
                                    .replaceAll("[\t\n\r]", " ");
                            log.info("DynamicTransactionManagerPlugin.intercept.sql|"+sql);
                            if (sql.matches(REGEX)) {
                                dataSourceType = DataSourceType.MASTER;
                            } else {
                                dataSourceType = DataSourceType.SLAVE;
                            }
                        }
                    } else {
                        dataSourceType = DataSourceType.MASTER;
                    }
                    //
                    log.debug("设置方法[{}] use [{}] Strategy, SqlCommandType [{}]..", ms.getId(),
                            dataSourceType.name());
                    log.debug("设置方法[{}] use [{}] Strategy, SqlCommandType [{}]..", ms
                            .getSqlCommandType().name());
                    cacheMap.put(ms.getId(), dataSourceType);
                }
                DataSourceContextHolder.setDataSourceType(dataSourceType);
            }
            return invocation.proceed();
        }
    
        @Override
        public Object plugin(Object target) {
            // TODO Auto-generated method stub
            log.info("DynamicTransactionManagerPlugin.plugin");
            if (target instanceof Executor) {
                return Plugin.wrap(target, DynamicTransactionManagerPlugin.this);
            } else {
                return target;
            }
        }
    
        @Override
        public void setProperties(Properties properties) {
            // TODO Auto-generated method stub
            log.info("DynamicTransactionManagerPlugin.setProperties");
        }
    
    }

    步骤6、mybatis 文件配置拦截器

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE configuration     PUBLIC "-//mybatis.org//DTD Config 3.0//EN"     "http://mybatis.org/dtd/mybatis-3-config.dtd">
    <configuration>
    
    
    
        <!-- mybatis配置信息 -->
        <settings>
            <setting name="lazyLoadingEnabled" value="true" />
            <!-- 全局性设置懒加载。如果设为‘false’,则所有相关联的都会被初始化加载 -->
    
            <setting name="cacheEnabled" value="true" />
            <!-- 对在此配置文件下的所有cache 进行全局性开/关设置 -->
    
            <setting name="aggressiveLazyLoading" value="false" />
            <!-- 当设置为‘true’的时候,懒加载的对象可能被任何懒属性全部加载。否则,每个属性都按需加载 -->
    
            <setting name="useGeneratedKeys" value="true" />
            <!-- 为了true,这个设置将强制使用被生成的主键,有一些驱动器不兼容不过仍然可以执行 -->
    
            <setting name="defaultExecutorType" value="REUSE" />
            <!-- 配置默认的执行器.SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(prepared statements); BATCH 
                执行器将重用语句并执行批量更新 -->
    
            <setting name="logImpl" value="LOG4J" />
            <!-- 指定 MyBatis 所用日志的具体实现,未指定时将自动查找 -->
        </settings>
    
    
    
        <typeAliases>
               ***
        </typeAliases>
    
        <plugins>
            <plugin interceptor="com.zyh.domain.base.DynamicTransactionManagerPlugin" />
        </plugins>
    
    </configuration>

    这个地方需要注意的是mybatis文件配置对顺序要求十分严格 setting typeAliases plugins的顺序不可变化(顺序固定)

    ^_^ 现在我们已经完成项目的整个配置操作,当我们执行读操作的时候mybatis拦截器会自动将数据源切换为从数据库,而写操作则会切换到主数据库。

  • 相关阅读:
    get started with laravel
    redis消息队列
    javascript模板引擎Mustache
    YIi 权限管理和基于角色的访问控制
    Yii CDbCriteria
    C++ 推断进程是否存在
    IE浏览器开启对JavaScript脚本的支持
    最小公约数(欧几里得算法&amp;&amp;stein算法)
    Nyoj 43 24 Point game 【DFS】
    【蓝桥杯】PREV-5 错误票据
  • 原文地址:https://www.cnblogs.com/supperlhg/p/9237594.html
Copyright © 2020-2023  润新知