原文地址:Spring AOP 实现读写分离

博客地址:http://www.extlight.com

一、前言

上一篇《MySQL 实现主从复制》 文章中介绍了 MySQL 主从复制的搭建,为了在项目上契合数据库的主从架构,本篇将介绍在应用层实现对数据库的读写分离。

二、原理

配置主从数据源,当接收请求时,执行具体方法之前(拦截),判断请求具体操作(读或写),最终确定从哪个数据源获取连接访问数据库。

在 JavaWeb 开发中,有 3 种方式可以对请求进行拦截:

filter:拦截所有请求
intercetor:拦截 handler/Action
aop 切面:依赖切入点

不难看出,使用 AOP 切面进行拦截最合理和灵活,因此本文将介绍使用 AOP 实现读写分离功能。

三、编码

本文只张贴关键性代码,详细代码请下载文章末尾源码进行查看。

3.1 代码

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
* @param key
*/
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方法之前执行
*
* @param point 切面对象
*/
public void before(JoinPoint point) {
// 获取到当前执行的方法名
String methodName = point.getSignature().getName();
if (isSlave(methodName)) {
// 标记为读库
DynamicDataSourceHolder.markSlave();
} else {
// 标记为写库
DynamicDataSourceHolder.markMaster();
}
} /**
* 判断是否为读库
*
* @param methodName
* @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 DynamicDataSource extends AbstractRoutingDataSource{ @Override
protected Object determineCurrentLookupKey() {
// 使用DynamicDataSourceHolder保证线程安全,并且得到当前线程中的数据源key
String dataSourceKey = DynamicDataSourceHolder.getDataSourceKey();
System.out.println("dataSourceKey ======> "+dataSourceKey);
return dataSourceKey;
} }

3.2 配置文件

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"?>
<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"
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-scan base-package="com.light.*">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan> <context:property-placeholder location="classpath:*.properties"/> <!-- 数据源 -->
<bean id="dataSource" class="com.light.dynamicdatasource.DynamicDataSource">
<property name="targetDataSources">
<map key-type="java.lang.String">
<entry key="master" value-ref="masterDataSource"></entry>
<entry key="slave" value-ref="slave01DataSource"></entry>
</map>
</property>
<!-- 默认数据源 -->
<property name="defaultTargetDataSource" ref="masterDataSource"/>
</bean> <!-- 主库数据源 -->
<bean id="masterDataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
<property name="url" value="${jdbc.master.url}"/>
<property name="username" value="${jdbc.master.username}"/>
<property name="password" value="${jdbc.master.password}"/>
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="initialSize" value="5"/>
<property name="minIdle" value="5"/>
<property name="maxActive" value="50"/>
</bean> <!-- 从库数据源 -->
<bean id="slave01DataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
<property name="url" value="${jdbc.slave01.url}"/>
<property name="username" value="${jdbc.slave01.username}"/>
<property name="password" value="${jdbc.slave01.password}"/>
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="initialSize" value="5"/>
<property name="minIdle" value="5"/>
<property name="maxActive" value="50"/>
</bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"></property>
<!-- 引入 mybatis 配置文件 -->
<property name="configLocation" value="classpath:mybatis/SqlMapConfig.xml"></property>
<property name="typeAliasesPackage" value="com.light.domain"></property>
<!-- sql配置文件 -->
<property name="mapperLocations" value="classpath:mybatis/mapper/*.xml"></property>
</bean> <!-- 扫描Mapper -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.light.mapper"></property>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>
</bean> <!-- 事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean> <!-- 通知 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!-- 传播行为 -->
<tx:method name="save*" propagation="REQUIRED"/>
<tx:method name="insert*" propagation="REQUIRED"/>
<tx:method name="delete*" propagation="REQUIRED"/>
<tx:method name="update*" propagation="REQUIRED"/>
<tx:method name="find*" propagation="SUPPORTS" read-only="true"/>
<tx:method name="get*" propagation="SUPPORTS" read-only="true"/>
<tx:method name="query*" propagation="SUPPORTS" read-only="true"/>
</tx:attributes>
</tx:advice> <!-- 切面 -->
<bean id="dataSourceAspect" class="com.light.dynamicdatasource.DataSourceAspect"></bean> <aop:config proxy-target-class="true">
<aop:pointcut id="myPointcut" expression="execution(* com.light.service.*.*(..))" />
<!-- 事务切面 -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="myPointcut"/>
<!-- 自定义切面 -->
<aop:aspect ref="dataSourceAspect" order="-9999">
<aop:before method="before" pointcut-ref="myPointcut" />
</aop:aspect>
</aop:config> <tx:annotation-driven transaction-manager="transactionManager"/> </beans>

四、测试

笔者在项目的 web 层写了 UserController 类,里边包含 get 和 delete 两个方法。

正常情况,当访问 get 方法(读操作)时,使用从库数据源,那么控制台应该打印 slave 。

正常情况,当访问 delete 方法(写操作)时,使用主库数据源,那么控制台应该打印 master 。

以下是 2 次测试结果:

get 方法:

delete 方法:

五、源码

源码下载

Spring AOP 实现读写分离的更多相关文章

  1. 使用Spring AOP实现读写分离(MySql实现主从复制)

    1.  背景 我们一般应用对数据库而言都是“读多写少”,也就说对数据库读取数据的压力比较大,有一个思路就是说采用数据库集群的方案,其中一个是主库,负责写入数据,我们称之为:写库: 其它都是从库,负责读 ...

  2. spring+mybatis实现读写分离

    springmore-core spring+ibatis实现读写分离 特点 无缝结合spring+ibatis,对于程序员来说,是透明的 除了修改配置信息之外,程序的代码不需要修改任何东西 支持sp ...

  3. 使用Spring实现MySQL读写分离

    1. 为什么要进行读写分离 大量的JavaWeb应用做的是IO密集型任务, 数据库的压力较大, 需要分流 大量的应用场景, 是读多写少, 数据库读取的压力更大 一个很自然的思路是使用一主多从的数据库集 ...

  4. 使用Spring实现MySQL读写分离(转)

    使用Spring实现MySQL读写分离 为什么要进行读写分离 大量的JavaWeb应用做的是IO密集型任务, 数据库的压力较大, 需要分流 大量的应用场景, 是读多写少, 数据库读取的压力更大 一个很 ...

  5. 基于spring的aop实现读写分离与事务配置

    项目开发中经常会遇到读写分离等多数据源配置的需求,在Java项目中可以通过Spring AOP来实现多数据源的切换. 一.Spring事务开启流程 Spring中通常通过@Transactional来 ...

  6. Spring+mybatis 实现aop数据库读写分离,多数据库源配置

    在数据库层面大都采用读写分离技术,就是一个Master数据库,多个Slave数据库.Master库负责数据更新和实时数据查询,Slave库当然负责非实时数据查询.因为在实际的应用中,数据库都是读多写少 ...

  7. Spring 实现数据库读写分离

    随着互联网的大型网站系统访问量的增高,数据库访问压力方面不断的显现而出,所以许多公司在数据库层面采用读写分离技术,也就是一个master,多个slave.master负责数据的实时更新或实时查询,而s ...

  8. spring实现数据库读写分离

    现在大型的电子商务系统,在数据库层面大都采用读写分离技术,就是一个Master数据库,多个Slave数据库.Master库负责数据更新和实时数据查询,Slave库当然负责非实时数据查询.因为在实际的应 ...

  9. 使用Spring+MySql实现读写分离(二)spring整合多数据库

    紧接着上一章,因为现在做的项目还是以spring为主要的容器管理框架,所以写以下spring如何整合多个数据源. 1. 背景 我们一般应用对数据库而言都是“读多写少”,也就说对数据库读取数据的压力比较 ...

随机推荐

  1. Spring注解@Value

    本文参考自: https://blog.csdn.net/ryelqy/article/details/77453713 @Value能让我们在java代码中使用property文件的属性,使用@Va ...

  2. 20135302魏静静——Linux课程期中总结

    Linux期中总结 Linux课程第一周实验及总结:[http://www.cnblogs.com/20135302wei/p/5218607.html] 冯诺依曼体系结构的核心思想是存储程序计算机. ...

  3. 20145211黄志远《网络对抗》Exp9 Web安全基础实践

    20145211黄志远<网络对抗>Exp9 Web安全基础实践 基础问题回答 SQL注入攻击原理,如何防御? SQL注入攻击就是利用输入的机会构造自己期望的请求,比如破坏掉用户名或者密码验 ...

  4. Codeforces Round #394 (Div. 2) A,B,C,D,E

    A. Dasha and Stairs time limit per test 2 seconds memory limit per test 256 megabytes input standard ...

  5. CentOS环境Docker安装教程(官方推荐的docker三种方式安装)

    CentOS环境Docker安装教程(官方推荐的docker三种方式安装) 一.使用yum方式安装 1.安装依赖包 $ sudo yum install -y yum-utils device-map ...

  6. [转]VS2015编译的程序在其他机器上缺少msvcp120.dll

    http://www.lai18.com/content/1159618.html 1. 今天分享一个自己在开发过程中遇到的困难. 用VS2015开发了一个windows客户端(win32项目),在自 ...

  7. 使用触发器定义 WPF 控件的行为

    Expression Studio 4.0   其他版本 Expression Studio 3.0 Expression Studio 2.0   此主题尚未评级 - 评价此主题   在应用程序的生 ...

  8. 【hive】求日期是星期几

    在Hive原生版本中,目前并没有返回星期几的函数.除了利用java自己编写udf外,也可以利用现有hive函数实现. 方法格式: pmod(datediff('#date#', '任意年任意一个星期日 ...

  9. 001——php字符串中的字符串定义

    <?php /** * 一.定义字符串: * ''单引号 ""双引号 <<<定界符 */ /* $url='http://baidu.com'; $baid ...

  10. tortoiseGIT保存用户名密码

    虽然GIT可以使用SSH来免去输入用户名密码的麻烦,但是更多的人我相信还是比较喜欢使用tortoiseGIT. 使用HTTP模式的代码库可以通过保存用户名密码的方式来免去重复输入的麻烦. 首先安装gi ...