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攔截器會自動將數據源切換為從數據庫,而寫操作則會切換到主數據庫。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM