Spring+MyBatis实现数据库读写分离方案
Posted li150dan
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring+MyBatis实现数据库读写分离方案相关的知识,希望对你有一定的参考价值。
本文重点介绍两种方案实现读写分离,推荐第二种方案
方案一:
通过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注解
注意:注解是写在接口方法上还是实现类方法上要根据前面步骤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)
以上是关于Spring+MyBatis实现数据库读写分离方案的主要内容,如果未能解决你的问题,请参考以下文章