基于spring的aop实现多数据源动态切换

https://lanjingling.github.io/2016/02/15/spring-aop-dynamicdatasource/

基于spring的aop实现多数据源动态切换

发表于 2016-02-15   |   分类于 spring  |

一、多数据源动态切换原理

项目中我们经常会遇到多数据源的问题,尤其是数据同步或定时任务等项目更是如此;又例如:读写分离数据库配置的系统。

1、多数据源设置:

1)静态数据源切换:
一般情况下,我们可以配置多个数据源,然后为每个数据源写一套对应的sessionFactory和dao层代码(以hibernate为例,mybatis同理),——我们称之为静态数据源配置

2)动态数据源切换:
可看出在Dao层代码中写死了两个SessionFactory,这样日后如果再多一个数据源,还要改代码添加一个SessionFactory,显然这并不符合开闭原则。比较好的做法是,配置多个数据源,只对应一套sessionFactory,数据源之间可以动态切换。

2、动态数据源切换时,如何保证数据库的事务:

目前事务最灵活的方式,是使用spring的声明式事务,本质是利用了spring的aop,在执行数据库操作前后,加上事务处理。

spring的事务管理,是基于数据源的,所以如果要实现动态数据源切换,而且在同一个数据源中保证事务是起作用的话,就需要注意二者的顺序问题,即:在事物起作用之前就要把数据源切换回来。

举一个例子:web开发常见是三层结构:controller、service、dao。一般事务都会在service层添加,如果使用spring的声明式事物管理,在调用service层代码之前,spring会通过aop的方式动态添加事务控制代码,所以如果要想保证事物是有效的,那么就必须在spring添加事务之前把数据源动态切换过来,也就是动态切换数据源的aop要至少在service上添加,而且要在spring声明式事物aop之前添加.根据上面分析:

  • 最简单的方式是把动态切换数据源的aop加到controller层,这样在controller层里面就可以确定下来数据源了。不过,这样有一个缺点就是,每一个controller绑定了一个数据源,不灵活。对于这种:一个请求,需要使用两个以上数据源中的数据完成的业务时,就无法实现了。
  • 针对上面的这种问题,可以考虑把动态切换数据源的aop放到service层,但要注意一定要在事务aop之前来完成。这样,对于一个需要多个数据源数据的请求,我们只需要在controller里面注入多个service实现即可。但这种做法的问题在于,controller层里面会涉及到一些不必要的业务代码,例如:合并两个数据源中的list…
  • 此外,针对上面的问题,还可以再考虑一种方案,就是把事务控制到dao层,然后在service层里面动态切换数据源。

二、实例1:

本例子中,对不同数据源分包(package)管理,同一包下的代码使用了同一数据源。
1、写一个DynamicDataSource类继承AbstractRoutingDataSource,并实现determineCurrentLookupKey方法

1234567
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;public class DynamicDataSource extends AbstractRoutingDataSource {	@Override	protected Object determineCurrentLookupKey() {		return CustomerContextHolder.getCustomerType();	}}

2、利用ThreadLocal解决线程安全问题:

1234567891011121314
public class CustomerContextHolder {	public static final String DATA_SOURCE_A = "dataSource";	public static final String DATA_SOURCE_B = "dataSource2";	private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();	public static void setCustomerType(String customerType) {		contextHolder.set(customerType);	}	public static String getCustomerType() {		return contextHolder.get();	}	public static void clearCustomerType() {		contextHolder.remove();	}}

3、定义一个数据源切面类,通过aop来控制数据源的切换:

123456789101112
import org.aspectj.lang.JoinPoint;

public class DataSourceInterceptor {

public void setdataSourceMysql(JoinPoint jp) {		DatabaseContextHolder.setCustomerType("dataSourceMySql");	}

public void setdataSourceOracle(JoinPoint jp) {		DatabaseContextHolder.setCustomerType("dataSourceOracle");	}}

4、在spring的application.xml中配置多个dataSource:

1234567891011121314151617181920212223242526272829303132333435363738
<!-- 数据源1 --><bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">	       <property name="driverClassName" value="net.sourceforge.jtds.jdbc.Driver"></property>		<property name="url" value="jdbc:jtds:sqlserver://10.82.81.51:1433;databaseName=standards"></property>		<property name="username" value="youguess"></property>		<property name="password" value="youguess"></property></bean><!-- 数据源2 --><bean id="dataSource2" class="org.apache.commons.dbcp.BasicDataSource">	       <property name="driverClassName" value="net.sourceforge.jtds.jdbc.Driver"></property>		<property name="url" value="jdbc:jtds:sqlserver://10.82.81.52:1433;databaseName=standards"></property>		<property name="username" value="youguess"></property>		<property name="password" value="youguess"></property></bean>

<bean id="dataSource" class="com.core.DynamicDataSource">	<property name="targetDataSources">		<map key-type="java.lang.String">			<entry key="dataSourceMySql" value-ref="dataSourceMySql" />			<entry key="dataSourceOracle" value-ref="dataSourceOracle" />		</map>	</property>	<property name="defaultTargetDataSource" ref="dataSourceMySql" /></bean>

<!-- 动态数据源切换aop 先与事务的aop  --><bean id="dataSourceInterceptor" class="com.core.DataSourceInterceptor" /><aop:config>	<aop:aspect id="dataSourceAspect" ref="dataSourceInterceptor">		<aop:pointcut id="dsMysql" expression="execution(* com.service.mysql..*.*(..))" />		<aop:pointcut id="dsOracle" expression="execution(* com.service.oracle..*.*(..))" />		<aop:before method="setdataSourceMysql" pointcut-ref="dsMysql"/>		<aop:before method="setdataSourceOracle" pointcut-ref="dsOracle"/>	</aop:aspect></aop:config>

<!-- 事物aop -->。。。

三、实例2:

该例子,实现了在业务逻辑层控制了mysql的读写分离。同样,使用了spring的aop来动态切换读和写的数据源。和上个例子不同之处在于:不同数据源没有按照包分类管理,而是使用了自定义注解。

1、首先配置mysql 的主从复制:
详情见,这里。

2、自定义注解:

123456789101112131415
import java.lang.annotation.ElementType;import java.lang.annotation.Target;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;/** * RUNTIME * 编译器将把注释记录在类文件中,在运行时 VM 将保留注释,因此可以反射性地读取。 * @author yangGuang * */@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)public @interface DataSource {	String value();}

3、基于spring的aop实现多数据源(读和写两个数据源):
1)写一个ChooseDataSource: 类继承AbstractRoutingDataSource,并实现determineCurrentLookupKey方法

12345678910
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class ChooseDataSource extends AbstractRoutingDataSource {

@Override	protected Object determineCurrentLookupKey() {		return HandleDataSource.getDataSource();	}

}

2)利用ThreadLocal解决线程安全问题:

12345678910
public class HandleDataSource {	public static final ThreadLocal<String> holder = new ThreadLocal<String>();	public static void putDataSource(String datasource) {		holder.set(datasource);	}

public static String getDataSource() {		return holder.get();	}    }

3)定义一个数据源切面类,通过aop访问,获取方法上的自定义注解,然后根据注解内容尽情判断,动态设置数据源:

123456789101112131415161718192021222324252627282930313233343536
import java.lang.reflect.Method;import org.aspectj.lang.JoinPoint;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.aspectj.lang.annotation.Pointcut;import org.aspectj.lang.reflect.MethodSignature;import org.springframework.stereotype.Component;//@Aspect//@Componentpublic class DataSourceAspect {	//@Pointcut("execution(* com.apc.cms.service.*.*(..))")  	public void pointCut(){};  

//  @Before(value = "pointCut()")	 public void before(JoinPoint point)		{			Object target = point.getTarget();			System.out.println(target.toString());			String method = point.getSignature().getName();			System.out.println(method);			Class<?>[] classz = target.getClass().getInterfaces();			Class<?>[] parameterTypes = ((MethodSignature) point.getSignature())				.getMethod().getParameterTypes();			try {				Method m = classz[0].getMethod(method, parameterTypes);				System.out.println(m.getName());				if (m != null && m.isAnnotationPresent(DataSource.class)) {					DataSource data = m.getAnnotation(DataSource.class);					HandleDataSource.putDataSource(data.value());				}

} catch (Exception e) {				e.printStackTrace();			}		}}

4)配置applicationContext.xml:

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
<!-- 主库数据源 --> <bean id="writeDataSource" class="com.jolbox.bonecp.BoneCPDataSource"  destroy-method="close">	<property name="driverClass" value="com.mysql.jdbc.Driver"/>	<property name="jdbcUrl" value="jdbc:mysql://172.22.14.6:3306/cpp?autoReconnect=true"/>	<property name="username" value="root"/>	<property name="password" value="root"/>	<property name="partitionCount" value="4"/>	<property name="releaseHelperThreads" value="3"/>	<property name="acquireIncrement" value="2"/>	<property name="maxConnectionsPerPartition" value="40"/>	<property name="minConnectionsPerPartition" value="20"/>	<property name="idleMaxAgeInSeconds" value="60"/>	<property name="idleConnectionTestPeriodInSeconds" value="60"/>	<property name="poolAvailabilityThreshold" value="5"/></bean>

<!-- 从库数据源 --><bean id="readDataSource" class="com.jolbox.bonecp.BoneCPDataSource"  destroy-method="close">	<property name="driverClass" value="com.mysql.jdbc.Driver"/>	<property name="jdbcUrl" value="jdbc:mysql://172.22.14.7:3306/cpp?autoReconnect=true"/>	<property name="username" value="root"/>	<property name="password" value="root"/>	<property name="partitionCount" value="4"/>	<property name="releaseHelperThreads" value="3"/>	<property name="acquireIncrement" value="2"/>	<property name="maxConnectionsPerPartition" value="40"/>	<property name="minConnectionsPerPartition" value="20"/>	<property name="idleMaxAgeInSeconds" value="60"/>	<property name="idleConnectionTestPeriodInSeconds" value="60"/>	<property name="poolAvailabilityThreshold" value="5"/></bean>

<!-- transaction manager, 事务管理 --><bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">	<property name="dataSource" ref="dataSource" /></bean>

<!-- 注解自动载入 --><context:annotation-config />

<!--enale component scanning (beware that this does not enable mapper scanning!)--><context:component-scan base-package="com.apc.cms.persistence.rdbms" /><context:component-scan base-package="com.apc.cms.service"> <context:include-filter type="annotation"  	expression="org.springframework.stereotype.Component" />  </context:component-scan> 

<context:component-scan base-package="com.apc.cms.auth" />

<!-- enable transaction demarcation with annotations --><tx:annotation-driven />

<!-- define the SqlSessionFactory --><bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">	<property name="dataSource" ref="dataSource" />	<property name="typeAliasesPackage" value="com.apc.cms.model.domain" /></bean>

<!-- scan for mappers and let them be autowired --><bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">	<property name="basePackage" value="com.apc.cms.persistence" />	<property name="sqlSessionFactory" ref="sqlSessionFactory" /></bean>

<bean id="dataSource" class="com.apc.cms.utils.ChooseDataSource">	<property name="targetDataSources">  		  <map key-type="java.lang.String">  			  <!-- write -->			 <entry key="write" value-ref="writeDataSource"/>  			 <!-- read -->			 <entry key="read" value-ref="readDataSource"/>  		  </map>  

</property>  	<property name="defaultTargetDataSource" ref="writeDataSource"/>  </bean>

<!-- 激活自动代理功能 --><aop:aspectj-autoproxy proxy-target-class="true"/>

<!-- 配置数据库注解aop --><bean id="dataSourceAspect" class="com.apc.cms.utils.DataSourceAspect" /><aop:config>	<aop:aspect id="c" ref="dataSourceAspect">		<aop:pointcut id="tx" expression="execution(* com.apc.cms.service..*.*(..))"/>		<aop:before pointcut-ref="tx" method="before"/>	</aop:aspect></aop:config><!-- 配置数据库注解aop -->

4、测试:

123456789
@DataSource("write")  public void update(User user) {      userMapper.update(user);  }  

@DataSource("read")  public Document getDocById(long id) {      return documentMapper.getById(id);  }
  • 测试写操作:可以通过应用修改数据,修改主库数据,发现从库的数据被同步更新了,所以定义的write操作都是走的写库
  • 测试读操作: 后台修改从库数据,查看主库的数据没有被修改,在应用页面中刷新,发现读的是从库的数据,说明读写分离ok。

#javaEE #spring

时间: 2024-10-23 07:31:23

基于spring的aop实现多数据源动态切换的相关文章

基于Struts2 Spring ibatis Oracle10g架构 多数据源动态切换实例

一.概述 基于Spring动态配置多数据源,在大型的应用中对数据进行切分,并且采用多个数据库实例进行管理,这样可以有效的提高系统的水平伸缩性,而这样的解决方案就会补同于常见的单一数据实例的方案,这就要程序在运行时根据当时的请求以及系统状态来动态的决定将数据存储在哪个数据库实例中,以及从哪个数据库提取数据. Spring配置多个数据源的方式和具体使用过程,Spring对于多数据源,以数据库表为参照,大体上可以分为两大类情况: 1.表级上的跨数据库,即对于不同的数据库却有不相同的表(表名和表结构完全

spring 数据源动态切换 与dubbo服务

1:问题描述 项目用了spring数据源动态切换,服务用的是dubbo.在运行一段时间后程序异常,更新操作没有切换到主库上.这个问题在先调用读操作后再调用写操作会出现.经分析原因有3: 第一:当程序运行一段时间后调用duboo服务时,读操作与写操作有可能会在一个线程里,当这种情况出现时 2:数据源配置 <idclass>    <name>       <key-type>          <keyvalue-ref/>          <keyv

菜鸟学SSH(十四)——Spring容器AOP的实现原理——动态代理

之前写了一篇关于IOC的博客--<Spring容器IOC解析及简单实现>,今天再来聊聊AOP.大家都知道Spring的两大特性是IOC和AOP. IOC负责将对象动态的注入到容器,从而达到一种需要谁就注入谁,什么时候需要就什么时候注入的效果,可谓是招之则来,挥之则去.想想都觉得爽,如果现实生活中也有这本事那就爽歪歪了,至于有多爽,各位自己脑补吧:而AOP呢,它实现的就是容器的另一大好处了,就是可以让容器中的对象都享有容器中的公共服务.那么容器是怎么做到的呢?它怎么就能让在它里面的对象自动拥有它

Spring3.3 整合 Hibernate3、MyBatis3.2 配置多数据源/动态切换数据源 方法(转)

一.开篇 这里整合分别采用了Hibernate和MyBatis两大持久层框架,Hibernate主要完成增删改功能和一些单一的对象查询功能,MyBatis主要负责查询功能.所以在出来数据库方言的时候基本上没有什么问题,但唯一可能出现问题的就是在hibernate做添加操作生成主键策略的时候.因为我们都知道hibernate的数据库本地方言会针对不同的数据库采用不同的主键生成策略. 所以针对这一问题不得不采用自定义的主键生成策略,自己写一个主键生成器的表来维护主键生成方式或以及使用其他的方式来生成

springboot多数据源动态切换和自定义mybatis件分页插

1.配置多数据源 增加druid依赖 完整pom文件 数据源配置文件 route.datasource.driver-class-name= com.mysql.jdbc.Driver route.datasource.url= jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8 route.datasource.username= root route.datasource.password= 1234

koala 多数据源动态切换

koala 特点:多数据源动态切换,数据源分组和负载均衡(轮询) 项目源码以及demo:https://github.com/zeq9069/koala 请大家关注一下哦!!哈哈! 最近,在开发项目的时候后,要用到多个数据源的动态切换,于是就开发了koala 这个小小得框架,koala支持多数据源的切换,数据源的分组和负载均衡(轮询)! 只需要将jar引入到你自己的项目中之后,然后进行简单配置就可以使用了!!使用起来 非常方便,该项目提供了三个核心的注解@ChangeTo,@DataSource

springAOP实现基于注解的数据源动态切换

需求 代码实现读写数据库分离 武器 spring3.0以上版本 实现思路 1.继承org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource,自定义数据源路由. 2.实现数据源类型管理工具,诸如DBContextHolder,包含设置和读取当前数据源配置. 3.实现数据源切换的AOP. 4.自定义只读注解,诸如@ReadOnlyKey. 5.配置transactionManager,实现aop. 代码示例 1.自定义的

spring+mybatis多数据源动态切换

spring mvc+mybatis+多数据源切换 选取oracle,mysql作为例子切换数据源.oracle为默认数据源,在测试的action中,进行mysql和oracle的动态切换. web.xml <context-param> <param-name>webAppRootKey</param-name> <param-value>trac</param-value> </context-param> <!-- Spr

springboot添加多数据源 以及 动态添加数据源动态切换数据源

<!-- Druid 数据连接池依赖 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.6</version> </dependency> //指定使用Druid做数据源spring.datasource.type=com.alibaba.druid.pool.Dru