本文重点介绍两种方案实现读写分离,推荐第二种方案
方案一:
通过Spring AOP在Service业务层实现读写分离,在调用DAO数据层前定义切面,利用Spring的AbstractRoutingDataSource解决多数据源的问题,实现动态选择数据源
- 优点:通过注解的方法在Service业务层(接口或者实现类)每个方法上配置数据源,原有代码改动量少,支持多读,易扩展
- 缺点:需要在Service业务层(接口或者实现类)每个方法上配置注解,人工管理,容易出错
方案二:
如果后台结构是spring+mybatis,可以通过spring的AbstractRoutingDataSource和mybatis Plugin拦截器实现非常友好的读写分离,原有代码不需要任何改变
- 优点:原有代码不变,支持多读,易扩展
- 缺点:
下面就详细介绍这两种方案的具体实现,先贴上用Maven构建的SSM项目目录结构图:
方案一实现方式介绍:
1. 定义注解
package com.demo.annotation; import java.lang.annotation.*; /** * 自定义注解 * 动态选择数据源时使用 */ @Documented @Target(ElementType.METHOD) //可以应用于方法 @Retention(RetentionPolicy.RUNTIME) //标记的注释由JVM保留,因此运行时环境可以使用它 public @interface DataSourceChange { boolean slave() default false; }
2. 定义类DynamicDataSourceHolder
package com.demo.datasource; import lombok.extern.slf4j.Slf4j; /** * @ProjectName: ssm-maven * @Package: com.demo.datasource * @ClassName: DynamicDataSourceHolder * @Description: 设置和获取动态数据源KEY * @Author: LiDan * @Date: 2019/7/10 16:15 * @Version: 1.0 */ @Slf4j public class DynamicDataSourceHolder { /** * 线程安全,记录当前线程的数据源key */ private static ThreadLocal<String> contextHolder = new ThreadLocal<String>(); /** * 主库,只允许一个 */ public static final String DB_MASTER = "master"; /** * 从库,允许多个 */ public static final String DB_SLAVE = "slave"; /** * 获取当前线程的数据源 * @return */ public static String getDataSource() { String db = contextHolder.get(); if(db == null) { //默认是master库 db = DB_MASTER; } log.info("所使用的数据源为:" + db); return db; } /** * 设置当前线程的数据源 * @param dataSource */ public static void setDataSource(String dataSource) { contextHolder.set(dataSource); } /** * 清理连接类型 */ public static void clearDataSource() { contextHolder.remove(); } /** * 判断是否是使用主库,提高部分使用 * @return */ public static boolean isMaster() { return DB_MASTER.equals(getDataSource()); } }
3. 定义类DynamicDataSource继承自AbstractRoutingDataSource
package com.demo.datasource; import lombok.extern.slf4j.Slf4j; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; import org.springframework.util.ReflectionUtils; import javax.sql.DataSource; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.atomic.AtomicInteger; /** * @ProjectName: ssm-maven * @Package: com.demo.datasource * @ClassName: DynamicDataSource * @Description: 动态数据源实现读写分离 * @Author: LiDan * @Date: 2019/7/10 16:28 * @Version: 1.0 */ @Slf4j public class DynamicDataSource extends AbstractRoutingDataSource { /** * 获取读数据源方式,0:随机,1:轮询 */ private int readDataSourcePollPattern = 0; /** * 读数据源个数 */ private int slaveCount = 0; /** * 记录读库的key */ private List<Object> slaveDataSources = new ArrayList<Object>(0); /** * 轮询计数,初始为0,AtomicInteger是线程安全的 */ private AtomicInteger counter = new AtomicInteger(0); /** * 每次操作数据库都会调用此方法,根据返回值动态选择数据源 * 定义当前使用的数据源(返回值为动态数据源的key值) * @return */ @Override protected Object determineCurrentLookupKey() { //如果使用主库,则直接返回 if (DynamicDataSourceHolder.isMaster()) { return DynamicDataSourceHolder.getDataSource(); } int index = 0; //如果不是主库则选择从库 if(readDataSourcePollPattern == 1) { //轮询方式 index = getSlaveIndex(); } else { //随机方式 index = ThreadLocalRandom.current().nextInt(0, slaveCount); } log.info("选择从库索引:"+index); return slaveDataSources.get(index); } /** * 该方法会在Spring Bean 加载初始化的时候执行,功能和 bean 标签的属性 init-method 一样 * 把所有的slave库key放到slaveDataSources里 */ @SuppressWarnings("unchecked") @Override public void afterPropertiesSet() { super.afterPropertiesSet(); // 由于父类的resolvedDataSources属性是私有的子类获取不到,需要使用反射获取 Field field = ReflectionUtils.findField(AbstractRoutingDataSource.class, "resolvedDataSources"); // 设置可访问 field.setAccessible(true); try { Map<Object, DataSource> resolvedDataSources = (Map<Object, DataSource>) field.get(this); // 读库的数据量等于数据源总数减去写库的数量 this.slaveCount = resolvedDataSources.size() - 1; for (Map.Entry<Object, DataSource> entry : resolvedDataSources.entrySet()) { if (DynamicDataSourceHolder.DB_MASTER.equals(entry.getKey())) { continue; } slaveDataSources.add(entry.getKey()); } } catch (Exception e) { e.printStackTrace(); } } /** * 轮询算法实现 * @return */ private int getSlaveIndex() { long currValue = counter.incrementAndGet(); if (counter.get() > 9999) { //以免超出int范围 counter.set(0); //还原 } //得到的下标为:0、1、2、3…… int index = (int)(currValue % slaveCount); return index; } public void setReadDataSourcePollPattern(int readDataSourcePollPattern) { this.readDataSourcePollPattern = readDataSourcePollPattern; } }
4. 定义AOP切面类DynamicDataSourceAspect
package com.demo.aop; import com.demo.annotation.DataSourceChange; import com.demo.datasource.DynamicDataSourceHolder; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.reflect.MethodSignature; import java.lang.reflect.Method; /** * @ProjectName: ssm-maven * @Package: com.demo.aop * @ClassName: DynamicDataSourceAspect * @Description: 定义选择数据源切面 * @Author: LiDan * @Date: 2019/7/11 11:05 * @Version: 1.0 */ @Slf4j public class DynamicDataSourceAspect { /** * 目标方法执行前调用 * @param point */ public void before(JoinPoint point) { log.info("before"); //获取代理接口或者类 Object target = point.getTarget(); String methodName = point.getSignature().getName(); //获取目标类的接口,所以注解@DataSourceChange需要写在接口里面 //Class<?>[] clazz = target.getClass().getInterfaces(); //获取目标类,所以注解@DataSourceChange需要写在类里面 Class<?>[] clazz = new Class<?>[]{target.getClass()}; Class<?>[] parameterTypes = ((MethodSignature) point.getSignature()).getMethod().getParameterTypes(); try { Method method = clazz[0].getMethod(methodName, parameterTypes); //判断方法上是否使用了该注解 if (method != null && method.isAnnotationPresent(DataSourceChange.class)) { DataSourceChange data = method.getAnnotation(DataSourceChange.class); if (data.slave()) { DynamicDataSourceHolder.setDataSource(DynamicDataSourceHolder.DB_SLAVE); } else { DynamicDataSourceHolder.setDataSource(DynamicDataSourceHolder.DB_MASTER); } } } catch (Exception ex) { log.error(String.format("Choose DataSource error, method:%s, msg:%s", methodName, ex.getMessage())); } } /** * 目标方法执行后调用 * @param point */ public void after(JoinPoint point) { log.info("after"); DynamicDataSourceHolder.clearDataSource(); } /** * 环绕通知 * @param joinPoint * @return */ public Object around(ProceedingJoinPoint joinPoint) { log.info("around"); Object result = null; //获取代理接口或者类 Object target = joinPoint.getTarget(); String methodName = joinPoint.getSignature().getName(); //获取目标类的接口,所以注解@DataSourceChange需要写在接口上 //Class<?>[] clazz = target.getClass().getInterfaces(); //获取目标类,所以注解@DataSourceChange需要写在类里面 Class<?>[] clazz = new Class<?>[]{target.getClass()}; Class<?>[] parameterTypes = ((MethodSignature) joinPoint.getSignature()).getMethod().getParameterTypes(); try { Method method = clazz[0].getMethod(methodName, parameterTypes); //判断方法上是否使用了该注解 if (method != null && method.isAnnotationPresent(DataSourceChange.class)) { DataSourceChange data = method.getAnnotation(DataSourceChange.class); if (data.slave()) { DynamicDataSourceHolder.setDataSource(DynamicDataSourceHolder.DB_SLAVE); } else { DynamicDataSourceHolder.setDataSource(DynamicDataSourceHolder.DB_MASTER); } } System.out.println("--环绕通知开始--开启事务--自动--"); long start = System.currentTimeMillis(); //调用 proceed() 方法才会真正的执行实际被代理的目标方法 result = joinPoint.proceed(); long end = System.currentTimeMillis(); System.out.println("总共执行时长" + (end - start) + " 毫秒"); System.out.println("--环绕通知结束--提交事务--自动--"); } catch (Throwable ex) { System.out.println("--环绕通知--出现错误"); log.error(String.format("Choose DataSource error, method:%s, msg:%s", methodName, ex.getMessage())); } finally { DynamicDataSourceHolder.clearDataSource(); } return result; } }
5. 配置spring-mybatis.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <!-- 自动扫描 --> <!--<context:component-scan base-package="com.demo.dao" />--> <!-- 引入配置文件 --> <context:property-placeholder location="classpath:properties/jdbc.properties"/> <!--<bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">--> <!--<property name="location" value="classpath:properties/jdbc.properties" />--> <!--</bean>--> <!-- DataSource数据库配置--> <bean id="abstractDataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="${jdbc.driver}"/> </bean> <!-- 写库配置--> <bean id="dataSourceMaster" parent="abstractDataSource"> <property name="driverClassName" value="${jdbc.driver}"/> <property name="url" value="${jdbc.master.url}"/> <property name="username" value="${jdbc.master.username}"/> <property name="password" value="${jdbc.master.password}"/> </bean> <!-- 从库一配置--> <bean id="dataSourceSlave1" parent="abstractDataSource"> <property name="driverClassName" value="${jdbc.driver}"/> <property name="url" value="${jdbc.slave.one.url}"/> <property name="username" value="${jdbc.slave.one.username}"/> <property name="password" value="${jdbc.slave.one.password}"/> </bean> <!-- 从库二配置--> <bean id="dataSourceSlave2" parent="abstractDataSource"> <property name="driverClassName" value="${jdbc.driver}"/> <property name="url" value="${jdbc.slave.two.url}"/> <property name="username" value="${jdbc.slave.two.username}"/> <property name="password" value="${jdbc.slave.two.password}"/> </bean> <!-- 设置自己定义的动态数据源 --> <bean id="dataSource" class="com.demo.datasource.DynamicDataSource"> <!-- 设置动态切换的多个数据源 --> <property name="targetDataSources"> <map> <!-- 这个key需要和程序中的key一致 --> <entry value-ref="dataSourceMaster" key="master"></entry> <entry value-ref="dataSourceSlave1" key="slave1"></entry> <entry value-ref="dataSourceSlave2" key="slave2"></entry> </map> </property> <!-- 设置默认的数据源,这里默认走写库 --> <property name="defaultTargetDataSource" ref="dataSourceMaster"/> <!-- 轮询方式 0:随机,1:轮询 --> <property name="readDataSourcePollPattern" value="1" /> </bean> <!-- spring和MyBatis完美整合,不需要mybatis的配置映射文件 --> <!--<bean id="mySqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">--> <bean id="mySqlSessionFactory" class="com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean"> <!--mybatis的配置文件--> <!--<property name="configLocation" value="classpath:beans/mybatis-config.xml"/>--> <!-- 自动扫描sqlMapper下面所有xml文件 --> <property name="mapperLocations"> <list> <value>classpath:sqlmapper/**/*.xml</value> </list> </property> <property name="dataSource" ref="dataSource"/> <property name="typeAliasesPackage" value="com.demo.model"/> </bean> <!-- DAO接口所在包名,Spring会自动查找其下的类 --> <bean id="daoMapper" class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="sqlSessionFactoryBeanName" value="mySqlSessionFactory"></property> <property name="basePackage" value="com.demo.dao"/> </bean> <!-- JDBC事务管理器 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!--<bean id="transactionManager" class="com.demo.datasource.DynamicDataSourceTransactionManager">--> <property name="dataSource" ref="dataSource"/> <property name="rollbackOnCommitFailure" value="true"/> </bean> <!-- 开启事务管理器的注解 --> <tx:annotation-driven transaction-manager="transactionManager" /> </beans>
6. 配置spring-aop.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- 配置动态选择数据库全自动方式aop --> <!--定义切面类--> <bean id="dynamicDataSourceAspect" class="com.demo.aop.DynamicDataSourceAspect" /> <aop:config> <!--定义切点,就是要监控哪些类下的方法--> <!--说明:该切点不能用于dao层,因为无法提前拦截到动态选择的数据源--> <aop:pointcut id="myPointCut" expression="execution(* com.demo.service..*.*(..))"/> <!--order表示切面顺序(多个切面时或者和JDBC事务管理器同时用时)--> <aop:aspect ref="dynamicDataSourceAspect" order="1"> <aop:before method="before" pointcut-ref="myPointCut"/> <aop:after method="after" pointcut-ref="myPointCut"/> <!--<aop:around method="around" pointcut-ref="myPointCut"/>--> </aop:aspect> </aop:config> <!-- 配置动态选择数据库全自动方式aop --> <!-- 启动AspectJ支持,开启自动注解方式AOP 使用配置注解,首先我们要将切面在spring上下文中声明成自动代理bean 默认情况下会采用JDK的动态代理实现AOP(只能对实现了接口的类生成代理,而不能针对类) 如果proxy-target-class="true" 声明时强制使用cglib代理(针对类实现代理) --> <!--<aop:aspectj-autoproxy proxy-target-class="true"/>--> </beans>
注意在applicationContext.xml中导入这两个xml
<!-- 导入mybatis配置文件 --> <import resource="classpath:beans/spring-mybatis.xml"></import> <!-- 导入spring-aop配置文件 --> <import resource="classpath:beans/spring-aop.xml"></import>
最后可以在Service业务层接口或者实现类具体方法上打注解@DataSourceChange(slave = true)
注意:注解是写在接口方法上还是实现类方法上要根据前面步骤4定义aop切面时获取注解的方式定
package com.demo.serviceimpl; import com.demo.annotation.DataSourceChange; import com.demo.dao.CmmAgencyDao; import com.demo.dao.CmmAgencystatusDao; import com.demo.model.bo.TCmmAgencyBO; import com.demo.model.bo.TCmmAgencystatusBO; import com.demo.model.po.TCmmAgencyPO; import com.demo.service.AgencyService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; /** * @ProjectName: ssm-maven * @Package: com.demo.serviceimpl * @ClassName: AgencyServiceImpl * @Description: 业务逻辑实现层 * @Author: LiDan * @Date: 2019/6/18 17:41 * @Version: 1.0 */ @Slf4j @Service public class AgencyServiceImpl implements AgencyService { @Autowired private CmmAgencyDao cmmAgencyDao; @Autowired private CmmAgencystatusDao cmmAgencystatusDao; /** * 查询信息 * @param bussnum * @return */ @Override @DataSourceChange(slave = true) //读库 @Transactional(readOnly = true) //指定事务是否为只读取数据:只读 public TCmmAgencyPO selectAgencyByBussNum(String bussnum) { } /** * 修改信息 * @param bussnum * @return */ @Override @Transactional(rollbackFor = Exception.class) //声明式事务控制 public boolean updateAgencyByBussNum(String bussnum) { } }
方案二实现方式介绍:
沿用上面方案一中的步骤2和3,并且去掉spring-aop切面和去掉业务层方法上面的自定义注解@DataSourceChange,再通过mybatis Plugin配置文件单独实现,或者配合自定义JDBC事务管理器来实现动态选择数据源
注意说明一下:如果业务方法上面没有打事务注解@Transactional,则默认直接通过mybatis Plugin拦截切面根据SQL语句动态选择数据源,
但是如果业务方法上面打上事务注解,则会首先通过JDBC事务管理器来动态选择数据源,然后才进入mybatis Plugin拦截切面选择数据源。
通过测试后发现如果业务方法上使用事务注解,则在启用事务时就确定了数据源,后面mybatis Plugin拦截已经没效果了,其实就是事务优先的原则,同一个事务操作过程中不可能再修改数据源了。
方案一中xml里配置切面时指定属性order="1"也是为了让碰到事务时让切面优先事务执行拦截
mybatis-config.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD SQL Map Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <plugins> <!--对mybatis中操作进行拦截,动态选择数据源--> <plugin interceptor="com.demo.aop.DynamicDataSourcePlugin"></plugin> </plugins> </configuration>
需要新建自定义JDBC事务管理器DynamicDataSourceTransactionManager
package com.demo.datasource; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.TransactionDefinition; /** * @ProjectName: ssm-maven * @Package: com.demo.datasource * @ClassName: DynamicDataSourceTransactionManager * @Description: 自定义JDBC事务管理器,动态选择数据源 * @Author: LiDan * @Date: 2019/7/15 17:33 * @Version: 1.0 */ public class DynamicDataSourceTransactionManager extends DataSourceTransactionManager { /** * 只读事务到读库,读写事务到写库 * @param transaction * @param definition */ @Override protected void doBegin(Object transaction, TransactionDefinition definition) { //获取事务的readOnly属性值 boolean readOnly = definition.isReadOnly(); if(readOnly) { DynamicDataSourceHolder.setDataSource(DynamicDataSourceHolder.DB_SLAVE); } else { DynamicDataSourceHolder.setDataSource(DynamicDataSourceHolder.DB_MASTER); } super.doBegin(transaction, definition); } /** * 清理本地线程的数据源 * @param transaction */ @Override protected void doCleanupAfterCompletion(Object transaction) { super.doCleanupAfterCompletion(transaction); DynamicDataSourceHolder.clearDataSource(); } }
并定义mybatis Plugin拦截切面DynamicDataSourcePlugin
package com.demo.aop; import com.demo.datasource.DynamicDataSourceHolder; import lombok.extern.slf4j.Slf4j; 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.*; import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds; import org.springframework.transaction.support.TransactionSynchronizationManager; import java.util.Locale; import java.util.Properties; /** * @ProjectName: ssm-maven * @Package: com.demo.aop * @ClassName: DynamicDataSourcePlugin * @Description: 对mybatis中操作进行拦截,增删改使用master,查询使用slave * @Author: LiDan * @Date: 2019/7/15 13:30 * @Version: 1.0 */ @Slf4j @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 DynamicDataSourcePlugin implements Interceptor { /** * sql匹配规则 */ private static final String REGEX = ".*insert\u0020.*|.*delete\u0020.*|.*update\u0020.*"; /** * 进行拦截操作,增删改和事务操作使用master,查询使用slave,里面有具体的实现代码,感兴趣可以学习mybatis源码去理解 * 你也可以根据自己的实际业务逻辑去控制 * @param invocation * @return * @throws Throwable */ @Override public Object intercept(Invocation invocation) throws Throwable { Object result = null; Object[] objects = invocation.getArgs(); MappedStatement mappedStatement = (MappedStatement) objects[0]; String lookupKey = DynamicDataSourceHolder.DB_MASTER; try { //是否使用事务管理 boolean syschronizationActive = TransactionSynchronizationManager.isActualTransactionActive(); if (!syschronizationActive) { //读方法 if (mappedStatement.getSqlCommandType().equals(SqlCommandType.SELECT)) { //如果selectKey为自增id查询主键(SELECT LAST INSERT_ID)方法,使用主库 if (mappedStatement.getId().contains(SelectKeyGenerator.SELECT_KEY_SUFFIX)) { lookupKey = DynamicDataSourceHolder.DB_MASTER; } else { BoundSql boundSql = mappedStatement.getSqlSource().getBoundSql(objects[1]); String sql = boundSql.getSql().toLowerCase(Locale.CHINA).replaceAll("[\t\n\r]", " "); //判断是否为“增删改” if (sql.matches(REGEX)) { lookupKey = DynamicDataSourceHolder.DB_MASTER; } else { lookupKey = DynamicDataSourceHolder.DB_SLAVE; } } } } else { //说明:如果启用事务管理器,那么这里就无法再修改数据源了,因为一旦启用事务时就确定了数据源(除非在自定义JDBC事务管理器类中重写doBegin方法来动态选择数据源) lookupKey = DynamicDataSourceHolder.DB_MASTER; } System.out.println("设置方法:"+mappedStatement.getId()+"; use:"+lookupKey+"; SqlCommanType:"+mappedStatement.getSqlCommandType().name()); DynamicDataSourceHolder.setDataSource(lookupKey); result = invocation.proceed(); } catch (Throwable ex) { log.error(String.format("Choose DataSource error, method:%s, msg:%s", mappedStatement.getId(), ex.getMessage())); } finally { DynamicDataSourceHolder.clearDataSource(); } return result; } /** * 设置拦截对象 * Executor在mybatis中是用来增删改查的,进行拦截 * @param target 拦截的对象 * @return */ @Override public Object plugin(Object target) { if (target instanceof Executor) { return Plugin.wrap(target, this); } else { return target; } } @Override public void setProperties(Properties properties) {} }