Spring + mybatis 主从数据库分离读写的几种方式(一)
Spring+mybatis 主从数据库分离读写(一)
——动态切换数据源方式
我们通过Spring AOP在业务层实现读写分离,也就是动态数据源的切换。在DAO层调用前定义切面,利用Spring的AbstractRoutingDataSource来解决多数据源的问题,用以实现动态选择数据源。我们可以通过注解实现自由切换DAO层接口指向的数据源。这样就使得代码变得极易扩展与便于阅读
步骤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、定义注解(必选)
我们使用注解是可以选择使用枚举类型,也可以选择直接使用数据源对应的key键值
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、数据源切换(必选)
package com.zyh.domain.base; import java.util.List; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; /** * 动态数据源 * * @author 1101399 * @CreateDate 2018-6-19 下午3:28:09 */ public class DynamicDataSource extends AbstractRoutingDataSource { /** * 返回数据源key值 TODO 数据源切换关键部分 */ @Override protected Object determineCurrentLookupKey() { boolean testSwith = false;// 如果注解输入字符串-true、枚举-false if(testSwith){ return DataSourceContextHolder.getDbType(); }else{ return DataSourceContextHolder.getDataSourceType().name(); } } }
在这里值的注意的是如果注解输入的类型是枚举类型的话
return DataSourceContextHolder.getDataSourceType();
不会实现数据源的切换(我想这也是使用该种方式进行数据库切换常见问题吧——至少我是看见好多地方都没有说到这一点),我们可以查看源代码
/** * Retrieve the current target DataSource. Determines the * {@link #determineCurrentLookupKey() current lookup key}, performs * a lookup in the {@link #setTargetDataSources targetDataSources} map, * falls back to the specified * {@link #setDefaultTargetDataSource default target DataSource} if necessary. * @see #determineCurrentLookupKey() */ protected DataSource determineTargetDataSource() { Assert.notNull(this.resolvedDataSources, "DataSource router not initialized"); Object lookupKey = determineCurrentLookupKey(); DataSource dataSource = this.resolvedDataSources.get(lookupKey); if (dataSource == null && (this.lenientFallback || lookupKey == null)) { dataSource = this.resolvedDefaultDataSource; } if (dataSource == null) { throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]"); } return dataSource; }
Object lookupKey = determineCurrentLookupKey();
DataSource dataSource = this.resolvedDataSources.get(lookupKey);
这两句代码是通过输入的数据库对应的键值实现切换数据源操作的,所以determineCurrentLookupKey()返回的最好是String类型的数据,说以我们如果使用枚举类型的注解信息输入我们最好是使用
return DataSourceContextHolder.getDataSourceType().name();
信息返回。
步骤6、Spring AOP 配置(必选)
Spring配置文件添加AOP切点设置
<aop:aspectj-autoproxy proxy-target-class="true" /> <bean id="manyDataSourceAspect" class="com.zyh.domain.base.DataSourceAspect" /> <aop:config> <aop:aspect id="dataSourceCutPoint" ref="manyDataSourceAspect"> <aop:pointcut expression="execution(* com.zyh.dao.*.*.*(..))" id="dataSourceCutPoint" /><!-- 配置切点 --> <aop:before pointcut-ref="dataSourceCutPoint" method="before" /> <aop:after pointcut-ref="dataSourceCutPoint" method="after" /> </aop:aspect> </aop:config>
单独一句,切点配置一定要仔细。而我像一个250一样切点处少了一个层级活活找了2天时间,唉头发都掉了一大把、我还是个孩子啊。(# ̄~ ̄#)
package com.zyh.domain.base; import java.lang.reflect.Method; import java.sql.Connection; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.reflect.MethodSignature; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.jdbc.datasource.DataSourceUtils; /** * 为AOP切面编程服务 为数据库动态切换配置 通过Spring配置AOP编程 Spring配置自动拦截相关操作 配合注解 * * @author 1101399 * @CreateDate 2018-6-20 上午9:47:43 */ public class DataSourceAspect extends DataSourceUtils{ /** * 配置控制台日志打印 */ private static final Logger LOG = LoggerFactory.getLogger(DataSourceAspect.class); /** * AOP切点对应的相关编程 * * Aspect:关注点的模块化。类似于类声明,包含PointCut和对应的Advice。在Spring * AOP中被定义为接口@Aspect,作用于TYPE(类、接口、方法、enum) * * JoinPoint:程序执行过程中明确的点,如方法的调用或特定的异常被抛出。 * 常用的是getArgs()用来获取参数,getTarget()获得目标对象。 * * 相关资料:https://www.cnblogs.com/sjlian/p/7325602.html * * @param point * @throws Throwable */ // ProceedingJoinPoint is only supported for around advice // ProceedingJoinPoint仅在around通知中受支持 public void before(JoinPoint point){ Object target = point.getTarget(); String method = point.getSignature().getName(); Class<?>[] classz = target.getClass().getInterfaces(); Class<?>[] parameterTypes = ((MethodSignature) point.getSignature()).getMethod() .getParameterTypes(); try { Method m = classz[0].getMethod(method, parameterTypes); if (m != null && m.isAnnotationPresent(DataSource.class)) { // 访问mapper中的注解 DataSource data = m.getAnnotation(DataSource.class);// 获得注解对象 switch (data.value()) { case MASTER: DataSourceContextHolder.setDataSourceType(DataSourceType.MASTER); LOG.info("using dataSource:{}", DataSourceType.MASTER); break; case SLAVE: DataSourceContextHolder.setDataSourceType(DataSourceType.SLAVE); LOG.info("using dataSource:{}", DataSourceType.SLAVE); break; default: break; } } else { ; } } catch (Exception e) { LOG.error("dataSource annotation error:{}", e.getMessage()); // 若出现异常,手动设为主库 DataSourceContextHolder.setDataSourceType(DataSourceType.MASTER); } finally { } } /** * AOP切口结束执行 * @param point */ public void after(JoinPoint point) { DataSourceContextHolder.clearDataSourceType(); } }
步骤7、使用注解实现数据源的切换(可选)
package com.zyh.dao.file; import org.apache.ibatis.annotations.Param; import org.springframework.stereotype.Repository; import com.zyh.dao.base.MybatisMapper; import com.zyh.domain.base.DataSource; import com.zyh.domain.base.DataSourceType; import com.zyh.domain.file.TXTFile; /** * TXT文件的数据库接口 * * @author 1101399 * @CreateDate 2018-4-13 上午8:44:50 */ @Repository(value="file.TXTFileMapper") public interface TXTFileMapper extends MybatisMapper<TXTFile,Integer>{ TXTFile findByName(@Param("name") String name); @DataSource(DataSourceType.SLAVE) TXTFile findDataById(@Param("id") Integer id); }
至于其他的数据源切换方式我们改日在谈(* ̄︶ ̄)
血肉苦弱机械飞升 :痛苦预示着超脱
本文来自博客园,作者:血肉苦弱机械飞升,转载请注明原文链接:https://www.cnblogs.com/supperlhg/articles/9235126.html