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拦截器会自动将数据源切换为从数据库,而写操作则会切换到主数据库。

posted @ 2018-06-28 10:53  血肉苦弱机械飞升  阅读(3772)  评论(0编辑  收藏  举报
跟随粒子特效