背景介绍 通常,在与数据库进行交互时,对数据库的操作都是“读多写少”,一方面,对数据库读取数据的压力比较大;另一方面,如果数据库分布在国内,那么在国外访问项目的时候,如果查询的接口较多,那么直接访问国内的数据库会大大的降低访问性能。因此,为了提升数据访问速度,缓解数据库的压力,我们可以在国外的服务器也安装一个mysql,部署一个项目,两个mysql进行主从配置,那么对于接口就需要采用读写分离策略,其基本思想是:将数据库分为主库和从库,主库只有一个,从库可有多个,主库主要负责写入数据,而从库则负责读取数据。
要求:
主从一致:读库和写库的数据一致;
写走主库:写数据必须写到写库;
读走从库:读数据必须到读库;
本文针对读写分离使用的方法是基于应用层实现,对原有代码的改动量较小,只是对配置文件进行了修改,下面来看具体实现。
原理 SSM框架将后台划分成了Dao、Service、Mapper层,读写分离的原理是在进入Service/Dao(具体哪一层,看配置项)之前,使用AOP来判断请求是前往写库还是读库,判断依据可以根据方法名判断,比如说以query、find、get等开头的就走读库,其他的走写库。
实现 定义动态数据源 实现通过集成Spring提供的AbstractRoutingDataSource,只需要实现determineCurrentLookupKey方法即可,由于DynamicDataSource是单例的,线程不安全的,所以采用ThreadLocal保证线程安全,由DynamicDataSourceHolder完成。
1.DynamicDataSource.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package com.test.dlab.aop.aspect;import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey () { return DynamicDataSourceHolder.getDataSourceKey(); } }
下面定义DynamicDataSourceHolder类,使用ThreadLocal技术来记录当前线程中的数据源的key。
2.DynamicDataSourceHolder.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 package com.test.dlab.aop.aspect;import org.apache.log4j.Logger;public class DynamicDataSourceHolder { private static Logger log = Logger.getLogger(DynamicDataSource.class ) ; private static final String MASTER = "master" ; private static final String SLAVE = "slave" ; private static final ThreadLocal<String> holder = new ThreadLocal<String>(); public static void setDataSourceKey (String key) { holder.set(key); } public static String getDataSourceKey () { return holder.get(); } public static void markAsMaster () { setDataSourceKey(MASTER); } public static void markAsSlave () { setDataSourceKey(SLAVE); } public static void clearDataSource () { log.info("移除clearDataSource" ); holder.remove(); } }
定义数据源的AOP切面 定义数据源的AOP切面,通过该Service的方法名判断是应该走读库还是写库。
DataSourceAspect.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 package com.test.dlab.aop.aspect;import org.apache.log4j.Logger;import org.aspectj.lang.JoinPoint;public class DataSourceAspect { private static final Logger log = Logger.getLogger(DataSourceAspect.class ) ; public void before (JoinPoint point) { String methodName = point.getSignature().getName(); if (isSlave(methodName)) { DynamicDataSourceHolder.markAsSlave(); } else { DynamicDataSourceHolder.markAsMaster(); } } private Boolean isSlave (String methodName) { log.info("根据Service方法名前缀判断是否走从库." ); return org.apache.commons.lang3.StringUtils.startsWithAny(methodName, "query" , "quer" , "find" , "get" ); } }
优化后的Aspect DataSourceAspect2.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 package com.test.dlab.aop.aspect;import java.util.ArrayList;import java.util.List;import java.util.Map;import org.apache.commons.lang3.StringUtils;import org.apache.log4j.Logger;import org.aspectj.lang.JoinPoint;import org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource;import org.springframework.transaction.interceptor.TransactionAttribute;import org.springframework.transaction.interceptor.TransactionAttributeSource;import org.springframework.transaction.interceptor.TransactionInterceptor;import org.springframework.util.PatternMatchUtils;import org.springframework.util.ReflectionUtils;import java.lang.reflect.Field;public class DataSourceAspect2 { private static final Logger log = Logger.getLogger(DataSourceAspect.class ) ; private List<String> slaveMethodPattern = new ArrayList<String>(); private static final String[] defaultSlaveMethodStart = new String[] { "quer" , "find" , "get" }; private String[] slaveMethodStart; @SuppressWarnings ("unchecked" ) public void setTxAdvice (TransactionInterceptor txAdvice) throws Exception { if (txAdvice == null ) { log.info("没有配置事务管理策略" ); return ; } TransactionAttributeSource transactionAttributeSource = txAdvice.getTransactionAttributeSource(); if (!(transactionAttributeSource instanceof NameMatchTransactionAttributeSource)) { return ; } NameMatchTransactionAttributeSource matchTransactionAttributeSource = (NameMatchTransactionAttributeSource) transactionAttributeSource; Field nameMapField = ReflectionUtils.findField(NameMatchTransactionAttributeSource.class, "nameMap"); log.info("nameMapField AAAAA:" + nameMapField); nameMapField.setAccessible(true ); Map<String, TransactionAttribute> map = (Map<String, TransactionAttribute>) nameMapField .get(matchTransactionAttributeSource); for (Map.Entry<String, TransactionAttribute> entry : map.entrySet()) { log.info("entity结果:" + entry.toString()); if (!entry.getValue().isReadOnly()) { continue ; } slaveMethodPattern.add(entry.getKey()); } } public void before (JoinPoint point) { String methodName = point.getSignature().getName(); log.info("方法名称:" + methodName); boolean isSlave = false ; if (slaveMethodPattern.isEmpty()) { log.info("当前Spring容器中没有配置事务策略,采用方法名匹配方式" ); isSlave = isSlave(methodName); } else { log.info("使用策略规则匹配" ); for (String mappedName : slaveMethodPattern) { if (isMatch(methodName, mappedName)) { isSlave = true ; break ; } } } if (isSlave) { log.info("标记为读库" ); DynamicDataSourceHolder.markAsSlave(); } else { log.info("标记为写库" ); DynamicDataSourceHolder.markAsMaster(); } } public void after (JoinPoint point) { DynamicDataSourceHolder.clearDataSource(); } private Boolean isSlave (String methodName) { log.info("根据Dao方法名前缀判断是否走从库." ); return StringUtils.startsWithAny(methodName, getSlaveMethodStart()); } protected boolean isMatch (String methodName, String mappedName) { return PatternMatchUtils.simpleMatch(mappedName, methodName); } public void setSlaveMethodStart (String[] slaveMethodStart) { this .slaveMethodStart = slaveMethodStart; } public String[] getSlaveMethodStart() { if (this .slaveMethodStart == null ) { return defaultSlaveMethodStart; } return slaveMethodStart; } }
修改配置文件 配置db.properties 在db.properties配置文件中增加主库和从库的信息,这里master为主库,slave为从库,具体内容如下:
1 2 3 4 5 6 7 8 9 jdbc.master.driver=com.mysql.jdbc.Driver jdbc.master.url=jdbc:mysql://127.0.0.1:3307/resource_test?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true jdbc.master.username=root jdbc.master.password=123456 jdbc.slave01.driver=com.mysql.jdbc.Driver jdbc.slave01.url=jdbc:mysql://127.0.0.1:3306/resource_test?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true jdbc.slave01.username=root jdbc.slave01.password=123456
修改applicationContext.xml 1)添加主从数据库连接池masterDataSource和slave01DataSource。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 <bean id ="masterDataSource" class ="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method ="close" > <property name ="driverClass" value ="${jdbc.master.driver}" /> <property name ="jdbcUrl" value ="${jdbc.master.url}" /> <property name ="user" value ="${jdbc.master.username}" /> <property name ="password" value ="${jdbc.master.password}" /> <property name ="initialPoolSize" value ="${jdbc.initialPoolSize}" /> <property name ="maxPoolSize" value ="${jdbc.maxPoolSize}" /> <property name ="minPoolSize" value ="${jdbc.minPoolSize}" /> <property name ="maxIdleTime" value ="${jdbc.maxIdleTime}" /> <property name ="acquireIncrement" value ="${jdbc.acquireIncrement}" /> <property name ="idleConnectionTestPeriod" value ="${jdbc.idleConnectionTestPeriod}" /> <property name ="acquireRetryAttempts" value ="${jdbc.acquireRetryAttempts}" /> <property name ="testConnectionOnCheckin" value ="true" /> <property name ="preferredTestQuery" value ="SELECT CURRENT_DATE" /> </bean > <bean id ="slave01DataSource" class ="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method ="close" > <property name ="driverClass" value ="${jdbc.slave01.driver}" /> <property name ="jdbcUrl" value ="${jdbc.slave01.url}" /> <property name ="user" value ="${jdbc.slave01.username}" /> <property name ="password" value ="${jdbc.slave01.password}" /> <property name ="initialPoolSize" value ="${jdbc.initialPoolSize}" /> <property name ="maxPoolSize" value ="${jdbc.maxPoolSize}" /> <property name ="minPoolSize" value ="${jdbc.minPoolSize}" /> <property name ="maxIdleTime" value ="${jdbc.maxIdleTime}" /> <property name ="acquireIncrement" value ="${jdbc.acquireIncrement}" /> <property name ="idleConnectionTestPeriod" value ="${jdbc.idleConnectionTestPeriod}" /> <property name ="acquireRetryAttempts" value ="${jdbc.acquireRetryAttempts}" /> <property name ="testConnectionOnCheckin" value ="true" /> <property name ="preferredTestQuery" value ="SELECT CURRENT_DATE" /> </bean >
2)定义DataSource
1 2 3 4 5 6 7 8 9 10 11 12 13 <bean id ="dataSource" class ="com.test.dlab.aop.aspect.DynamicDataSource" > <property name ="targetDataSources" > <map key-type ="java.lang.String" > <entry key ="master" value-ref ="masterDataSource" /> <entry key ="slave" value-ref ="slave01DataSource" /> </map > </property > <property name ="defaultTargetDataSource" ref ="masterDataSource" /> </bean >
3)配置事务管理器
1 2 3 4 <bean id ="transactionManager" class ="org.springframework.jdbc.datasource.DataSourceTransactionManager" > <property name ="dataSource" ref ="dataSource" /> </bean >
4)配置事务属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <tx:advice id ="txAdvice" transaction-manager ="transactionManager" > <tx:attributes > <tx:method name ="query*" read-only ="true" /> <tx:method name ="find*" read-only ="true" /> <tx:method name ="get*" read-only ="true" /> <tx:method name ="save*" propagation ="REQUIRED" /> <tx:method name ="update*" propagation ="REQUIRED" /> <tx:method name ="delete*" propagation ="REQUIRED" /> <tx:method name ="*" /> </tx:attributes > </tx:advice >
5)配置事务切面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <bean class ="com.test.dlab.aop.aspect.DataSourceAspect2" id ="dataSourceAspect" /> <aop:config expose-proxy ="true" > <aop:pointcut id ="txPointcut" expression ="execution(* com.test.dlab.service..*.*(..))" /> <aop:advisor advice-ref ="txAdvice" pointcut-ref ="txPointcut" /> <aop:aspect ref ="dataSourceAspect" order ="-9999" > <aop:pointcut id ="tx" expression ="execution(* com.test.dlab.dao..*.*(..))" /> <aop:before method ="before" pointcut-ref ="tx" /> </aop:aspect > </aop:config >
OK,启动项目,访问一切ok!
注意事项 1.由于实现的是dao层的读写分离,因此在配置aop的时候,应该去掉 proxy-target-class="true"
:
1 <aop:aspectj-autoproxy />
2.由于service层配置了事务,所以为了不影响dao层的主从分离,在配置service事务属性的时候,不能添加下列语句,否则数据库在service层进入事务之后,无法实现dao层的主从分离。
1 <tx:method name ="*" propagation ="REQUIRED" />