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