通常,在与数据库进行交互时,对数据库的操作都是“读多写少”,一方面,对数据库读取数据的压力比较大;另一方面,如果数据库分布在国内,那么在国外访问项目的时候,如果查询的接口较多,那么直接访问国内的数据库会大大的降低访问性能。因此,为了提升数据访问速度,缓解数据库的压力,我们可以在国外的服务器也安装一个mysql,部署一个项目,两个mysql进行主从配置,那么对于接口就需要采用读写分离策略,其基本思想是:将数据库分为主库和从库,主库只有一个,从库可有多个,主库主要负责写入数据,而从库则负责读取数据。
要求:
本文针对读写分离使用的方法是基于应用层实现,对原有代码的改动量较小,只是对配置文件进行了修改,下面来看具体实现。
SSM框架将后台划分成了Dao、Service、Mapper层,读写分离的原理是在进入Service/Dao(具体哪一层,看配置项)之前,使用AOP来判断请求是前往写库还是读库,判断依据可以根据方法名判断,比如说以query、find、get等开头的就走读库,其他的走写库。
实现通过集成Spring提供的AbstractRoutingDataSource,只需要实现determineCurrentLookupKey方法即可,由于DynamicDataSource是单例的,线程不安全的,所以采用ThreadLocal保证线程安全,由DynamicDataSourceHolder完成。
1.DynamicDataSource.java
package com.test.dlab.aop.aspect;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
*
* 定义动态数据源,实现通过集成Spring提供的AbstractRoutingDataSource,
* 只需要实现determineCurrentLookupKey方法即可
* 由于DynamicDataSource是单例的,线程不安全的,所以采用ThreadLocal保证线程安全,
* 由DynamicDataSourceHolder完成。
*
* @author liudiwei 2018年3月12日
*
*/
public class DynamicDataSource extends AbstractRoutingDataSource
{
@Override
protected Object determineCurrentLookupKey()
{
// 使用DynamicDataSourceHolder保证线程安全,并且得到当前线程中的数据源key
return DynamicDataSourceHolder.getDataSourceKey();
}
}
下面定义DynamicDataSourceHolder类,使用ThreadLocal技术来记录当前线程中的数据源的key。
2.DynamicDataSourceHolder.java
package com.test.dlab.aop.aspect;
import org.apache.log4j.Logger;
/**
*
* @author liudiwei 2018年3月12日 使用ThreadLocal技术来记录当前线程中的数据源的key
*
*/
public class DynamicDataSourceHolder
{
private static Logger log = Logger.getLogger(DynamicDataSource.class);
// 写库对应的数据源key
private static final String MASTER = "master";
// 读库对应的数据源key
private static final String SLAVE = "slave";
// 使用ThreadLocal记录当前线程的数据源key
private static final ThreadLocal<String> holder = new ThreadLocal<String>();
/**
* 设置数据源key
*
* @param key
*/
public static void setDataSourceKey(String key)
{
holder.set(key);
}
/**
* 获取数据源key
*
* @return
*/
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切面,通过该Service的方法名判断是应该走读库还是写库。
package com.test.dlab.aop.aspect;
import org.apache.log4j.Logger;
import org.aspectj.lang.JoinPoint;
/**
* 定义数据源的AOP切面,通过该Service的方法名判断是应该走读库还是写库
*
* @author liudiwei 2018年3月12日
*
*/
public class DataSourceAspect
{
private static final Logger log = Logger.getLogger(DataSourceAspect.class);
/**
* 在进入Service方法之前执行
*
* @param point
* 切面对象
*/
public void before(JoinPoint point)
{
// 获取到当前执行的方法名
String methodName = point.getSignature().getName();
if (isSlave(methodName))
{
// 标记为读库
DynamicDataSourceHolder.markAsSlave();
}
else
{
// 标记为写库
DynamicDataSourceHolder.markAsMaster();
}
}
/**
* 判断是否为读库
*
* @param methodName
* @return
*
*/
private Boolean isSlave(String methodName)
{
log.info("根据Service方法名前缀判断是否走从库.");
return org.apache.commons.lang3.StringUtils.startsWithAny(methodName, "query", "quer", "find", "get");
}
}
DataSourceAspect2.java
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;
/**
* 定义数据源的AOP切面,通过该Dao的方法名判断是应该走读库还是写库
*
* @author liudiwei 2018年3月12日
*
*/
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;
/**
* 读取事务管理中的策略
*
* @param txAdvice
* @throws Exception
*/
@SuppressWarnings("unchecked")
public void setTxAdvice(TransactionInterceptor txAdvice) throws Exception
{
if (txAdvice == null)
{
log.info("没有配置事务管理策略");
return;
}
// 从txAdvice获取到策略配置信息
TransactionAttributeSource transactionAttributeSource = txAdvice.getTransactionAttributeSource();
if (!(transactionAttributeSource instanceof NameMatchTransactionAttributeSource))
{
return;
}
// 使用反射技术获取到NameMatchTransactionAttributeSource对象中的nameMap属性值
NameMatchTransactionAttributeSource matchTransactionAttributeSource = (NameMatchTransactionAttributeSource) transactionAttributeSource;
Field nameMapField = ReflectionUtils.findField(NameMatchTransactionAttributeSource.class, "nameMap");
log.info("nameMapField AAAAA:" + nameMapField);
nameMapField.setAccessible(true); // 设置该字段可访问
// 获取nameMap的值
Map<String, TransactionAttribute> map = (Map<String, TransactionAttribute>) nameMapField
.get(matchTransactionAttributeSource);
// 遍历nameMap
for (Map.Entry<String, TransactionAttribute> entry : map.entrySet())
{
log.info("entity结果:" + entry.toString());
// 判断之后定义了ReadOnly的策略才加入到slaveMethodPattern
if (!entry.getValue().isReadOnly())
{
continue;
}
slaveMethodPattern.add(entry.getKey());
}
}
/**
* 在进入Service方法之前执行
*
* @param point
* 切面对象
*/
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();
}
/**
* 判断是否为读库
*
* @param methodName
* @return
*/
private Boolean isSlave(String methodName)
{
log.info("根据Dao方法名前缀判断是否走从库.");
return StringUtils.startsWithAny(methodName, getSlaveMethodStart());
}
/**
* 通配符匹配
*
* Return if the given method name matches the mapped name.
* <p>
* The default implementation checks for "xxx*", "*xxx" and "*xxx*" matches,
* as well as direct equality. Can be overridden in subclasses.
*
* @param methodName
* the method name of the class
* @param mappedName
* the name in the descriptor
* @return if the names match
* @see org.springframework.util.PatternMatchUtils#simpleMatch(String,
* String)
*/
protected boolean isMatch(String methodName, String mappedName)
{
return PatternMatchUtils.simpleMatch(mappedName, methodName);
}
/**
* 用户指定slave的方法名前缀
*
* @param slaveMethodStart
*/
public void setSlaveMethodStart(String[] slaveMethodStart)
{
this.slaveMethodStart = slaveMethodStart;
}
public String[] getSlaveMethodStart()
{
if (this.slaveMethodStart == null)
{
// 没有指定,使用默认
return defaultSlaveMethodStart;
}
return slaveMethodStart;
}
}
在db.properties配置文件中增加主库和从库的信息,这里master为主库,slave为从库,具体内容如下:
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
1)添加主从数据库连接池masterDataSource和slave01DataSource。
<!-- 配置连接池 -->
<bean id="masterDataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
<!-- 数据库驱动 -->
<property name="driverClass" value="${jdbc.master.driver}" />
<!-- 相应驱动的jdbcUrl -->
<property name="jdbcUrl" value="${jdbc.master.url}" />
<!-- 数据库的用户名 -->
<property name="user" value="${jdbc.master.username}" />
<!-- 数据库的密码 -->
<property name="password" value="${jdbc.master.password}" />
<!--初始化时获取三个连接,取值应在minPoolSize与maxPoolSize之间。Default: 3 -->
<property name="initialPoolSize" value="${jdbc.initialPoolSize}"/>
<!--连接池中保留的最大连接数。Default: 15 -->
<property name="maxPoolSize" value="${jdbc.maxPoolSize}"/>
<!--连接池中保留的最小连接数。 -->
<property name="minPoolSize" value="${jdbc.minPoolSize}"/>
<!--最大空闲时间,1800秒内未使用则连接被丢弃。若为0则永不丢弃。Default: 0 -->
<property name="maxIdleTime" value="${jdbc.maxIdleTime}" />
<!--当连接池中的连接耗尽的时候c3p0一次同时获取的连接数。Default: 3 -->
<property name="acquireIncrement" value="${jdbc.acquireIncrement}" />
<!--每0秒检查所有连接池中的空闲连接。Default: 0 -->
<property name="idleConnectionTestPeriod" value="${jdbc.idleConnectionTestPeriod}" />
<!--定义在从数据库获取新连接失败后重复尝试的次数。Default: 30 -->
<property name="acquireRetryAttempts" value="${jdbc.acquireRetryAttempts}" />
<!--如果设为true那么在取得连接的同时将校验连接的有效性。Default: false -->
<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}" />
<!-- 相应驱动的jdbcUrl -->
<property name="jdbcUrl" value="${jdbc.slave01.url}" />
<!-- 数据库的用户名 -->
<property name="user" value="${jdbc.slave01.username}" />
<!-- 数据库的密码 -->
<property name="password" value="${jdbc.slave01.password}" />
<!--初始化时获取三个连接,取值应在minPoolSize与maxPoolSize之间。Default: 3 -->
<property name="initialPoolSize" value="${jdbc.initialPoolSize}"/>
<!--连接池中保留的最大连接数。Default: 15 -->
<property name="maxPoolSize" value="${jdbc.maxPoolSize}"/>
<!--连接池中保留的最小连接数。 -->
<property name="minPoolSize" value="${jdbc.minPoolSize}"/>
<!--最大空闲时间,1800秒内未使用则连接被丢弃。若为0则永不丢弃。Default: 0 -->
<property name="maxIdleTime" value="${jdbc.maxIdleTime}" />
<!--当连接池中的连接耗尽的时候c3p0一次同时获取的连接数。Default: 3 -->
<property name="acquireIncrement" value="${jdbc.acquireIncrement}" />
<!--每0秒检查所有连接池中的空闲连接。Default: 0 -->
<property name="idleConnectionTestPeriod" value="${jdbc.idleConnectionTestPeriod}" />
<!--定义在从数据库获取新连接失败后重复尝试的次数。Default: 30 -->
<property name="acquireRetryAttempts" value="${jdbc.acquireRetryAttempts}" />
<!--如果设为true那么在取得连接的同时将校验连接的有效性。Default: false -->
<property name="testConnectionOnCheckin" value="true"/>
<property name="preferredTestQuery" value="SELECT CURRENT_DATE"/>
</bean>
2)定义DataSource
<!-- 定义数据源,使用自己实现的数据源 -->
<bean id="dataSource" class="com.test.dlab.aop.aspect.DynamicDataSource">
<!-- 设置多个数据源 -->
<property name="targetDataSources">
<map key-type="java.lang.String">
<!-- 这个key需要和程序中的key一致 -->
<entry key="master" value-ref="masterDataSource" />
<entry key="slave" value-ref="slave01DataSource" />
</map>
</property>
<!-- 设置默认的数据源,这里默认走写库 -->
<property name="defaultTargetDataSource" ref="masterDataSource" />
</bean>
3)配置事务管理器
<!-- 配置事务管理器bean -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
4)配置事务属性
<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)配置事务切面
<!-- 主从数据库配置 part2 start -->
<!-- 配置数据库注解aop -->
<bean class="com.test.dlab.aop.aspect.DataSourceAspect2" id="dataSourceAspect" />
<!-- 配置xml事务 切面 -->
<aop:config expose-proxy="true">
<aop:pointcut id="txPointcut" expression="execution(* com.test.dlab.service..*.*(..))" />
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
<!-- 将切面应用到自定义的切面处理器上,-9999保证该切面优先级最高执行 -->
<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>
<!-- 主从数据库配置 part2 end -->
OK,启动项目,访问一切ok!
1.由于实现的是dao层的读写分离,因此在配置aop的时候,应该去掉 proxy-target-class="true"
:
<aop:aspectj-autoproxy />
2.由于service层配置了事务,所以为了不影响dao层的主从分离,在配置service事务属性的时候,不能添加下列语句,否则数据库在service层进入事务之后,无法实现dao层的主从分离。
<tx:method name="*" propagation="REQUIRED" />