上一篇《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 方法: