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拦截器会自动将数据源切换为从数据库,而写操作则会切换到主数据库。
血肉苦弱机械飞升 :痛苦预示着超脱
本文来自博客园,作者:血肉苦弱机械飞升,转载请注明原文链接:https://www.cnblogs.com/supperlhg/articles/9237594.html