本文重點介紹兩種方案實現讀寫分離,推薦第二種方案
方案一:
通過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) {} }