上一篇《MySQL 实现主从复制》 文章中介绍了 MySQL 主从复制的搭建,为了在项目上契合数据库的主从架构,本篇将介绍在应用层实现对数据库的读写分离。
配置主从数据源,当接收请求时,执行具体方法之前(拦截),判断请求具体操作(读或写),最终确定从哪个数据源获取连接访问数据库。
在 JavaWeb 开发中,有 3 种方式可以对请求进行拦截:
filter:拦截所有请求 intercetor:拦截 handler/Action aop 切面:依赖切入点
不难看出,使用 AOP 切面进行拦截最合理和灵活,因此本文将介绍使用 AOP 实现读写分离功能。
1)DynamicDataSourceHolder 确保线程安全:
/**
*
* 使用ThreadLocal技术来记录当前线程中的数据源的key
*
*/
public class DynamicDataSourceHolder{
//写库对应的数据源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
*@paramkey
*/
public static void putDataSourceKey(String key){
holder.set(key);
}
/**
* 获取数据源key
*@return
*/
public static String getDataSourceKey(){
return holder.get();
}
/**
* 标记写库
*/
public static void markMaster(){
putDataSourceKey(MASTER);
}
/**
* 标记读库
*/
public static void markSlave(){
putDataSourceKey(SLAVE);
}
}
2)定义 AOP 切面判断当前线程的读写操作
/**
* 定义数据源的AOP切面,通过该Service的方法名判断是应该走读库还是写库
*
*/
public class DataSourceAspect{
/**
* 在进入Service方法之前执行
*
*@parampoint 切面对象
*/
public void before(JoinPoint point){
// 获取到当前执行的方法名
String methodName = point.getSignature().getName();
if (isSlave(methodName)) {
// 标记为读库
DynamicDataSourceHolder.markSlave();
} else {
// 标记为写库
DynamicDataSourceHolder.markMaster();
}
}
/**
* 判断是否为读库
*
*@parammethodName
*@return
*/
private Boolean isSlave(String methodName){
// 方法名以query、find、get开头的方法名走从库
return StringUtils.startsWithAny(methodName, "query", "find", "get");
}
}
3)定义动态数据源,确定最终使用的数据源:
/**
* 定义动态数据源,实现通过集成Spring提供的AbstractRoutingDataSource,只需要实现determineCurrentLookupKey方法即可
*
* 由于DynamicDataSource是单例的,线程不安全的,所以采用ThreadLocal保证线程安全,由DynamicDataSourceHolder完成。
*
*/
public class DynamicDataSourceextends AbstractRoutingDataSource{
@Override
protected Object determineCurrentLookupKey(){
// 使用DynamicDataSourceHolder保证线程安全,并且得到当前线程中的数据源key
String dataSourceKey = DynamicDataSourceHolder.getDataSourceKey();
System.out.println("dataSourceKey ======> "+dataSourceKey);
return dataSourceKey;
}
}
1)jdbc.properties
jdbc.driver=com.mysql.jdbc.Driver jdbc.master.url=jdbc:mysql://192.168.2.21/mysql_test?characterEncoding=utf-8&allowMultiQueries=true&serverTimezone=UTC jdbc.master.username=root jdbc.master.password=tiger jdbc.slave01.url=jdbc:mysql://192.168.2.22/mysql_test?characterEncoding=utf-8&allowMultiQueries=true&serverTimezone=UTC jdbc.slave01.username=root jdbc.slave01.password=tiger
2)applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beansxmlns="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"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">
<context:component-scanbase-package="com.light.*">
<context:exclude-filtertype="annotation"expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<context:property-placeholderlocation="classpath:*.properties"/>
<!-- 数据源 -->
<beanid="dataSource"class="com.light.dynamicdatasource.DynamicDataSource">
<propertyname="targetDataSources">
<mapkey-type="java.lang.String">
<entrykey="master"value-ref="masterDataSource"></entry>
<entrykey="slave"value-ref="slave01DataSource"></entry>
</map>
</property>
<!-- 默认数据源 -->
<propertyname="defaultTargetDataSource"ref="masterDataSource"/>
</bean>
<!-- 主库数据源 -->
<beanid="masterDataSource"class="com.alibaba.druid.pool.DruidDataSource"destroy-method="close">
<propertyname="url"value="${jdbc.master.url}"/>
<propertyname="username"value="${jdbc.master.username}"/>
<propertyname="password"value="${jdbc.master.password}"/>
<propertyname="driverClassName"value="${jdbc.driver}"/>
<propertyname="initialSize"value="5"/>
<propertyname="minIdle"value="5"/>
<propertyname="maxActive"value="50"/>
</bean>
<!-- 从库数据源 -->
<beanid="slave01DataSource"class="com.alibaba.druid.pool.DruidDataSource"destroy-method="close">
<propertyname="url"value="${jdbc.slave01.url}"/>
<propertyname="username"value="${jdbc.slave01.username}"/>
<propertyname="password"value="${jdbc.slave01.password}"/>
<propertyname="driverClassName"value="${jdbc.driver}"/>
<propertyname="initialSize"value="5"/>
<propertyname="minIdle"value="5"/>
<propertyname="maxActive"value="50"/>
</bean>
<beanid="sqlSessionFactory"class="org.mybatis.spring.SqlSessionFactoryBean">
<propertyname="dataSource"ref="dataSource"></property>
<!-- 引入 mybatis 配置文件 -->
<propertyname="configLocation"value="classpath:mybatis/SqlMapConfig.xml"></property>
<propertyname="typeAliasesPackage"value="com.light.domain"></property>
<!-- sql配置文件 -->
<propertyname="mapperLocations"value="classpath:mybatis/mapper/*.xml"></property>
</bean>
<!-- 扫描Mapper -->
<beanclass="org.mybatis.spring.mapper.MapperScannerConfigurer">
<propertyname="basePackage"value="com.light.mapper"></property>
<propertyname="sqlSessionFactoryBeanName"value="sqlSessionFactory"></property>
</bean>
<!-- 事务管理器 -->
<beanid="transactionManager"class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<propertyname="dataSource"ref="dataSource"/>
</bean>
<!-- 通知 -->
<tx:adviceid="txAdvice"transaction-manager="transactionManager">
<tx:attributes>
<!-- 传播行为 -->
<tx:methodname="save*"propagation="REQUIRED"/>
<tx:methodname="insert*"propagation="REQUIRED"/>
<tx:methodname="delete*"propagation="REQUIRED"/>
<tx:methodname="update*"propagation="REQUIRED"/>
<tx:methodname="find*"propagation="SUPPORTS"read-only="true"/>
<tx:methodname="get*"propagation="SUPPORTS"read-only="true"/>
<tx:methodname="query*"propagation="SUPPORTS"read-only="true"/>
</tx:attributes>
</tx:advice>
<!-- 切面 -->
<beanid="dataSourceAspect"class="com.light.dynamicdatasource.DataSourceAspect"></bean>
<aop:configproxy-target-class="true">
<aop:pointcutid="myPointcut"expression="execution(* com.light.service.*.*(..))"/>
<!-- 事务切面 -->
<aop:advisoradvice-ref="txAdvice"pointcut-ref="myPointcut"/>
<!-- 自定义切面 -->
<aop:aspectref="dataSourceAspect"order="-9999">
<aop:beforemethod="before"pointcut-ref="myPointcut"/>
</aop:aspect>
</aop:config>
<tx:annotation-driventransaction-manager="transactionManager"/>
</beans>
笔者在项目的 web 层写了 UserController 类,里边包含 get 和 delete 两个方法。
正常情况,当访问 get 方法(读操作)时,使用从库数据源,那么控制台应该打印 slave 。
正常情况,当访问 delete 方法(写操作)时,使用主库数据源,那么控制台应该打印 master 。
以下是 2 次测试结果:
get 方法:
delete 方法: