动态切换数据源理论知识

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

1、相信很多人都知道JDK代理,分静态代理和动态代理两种,同样的,多数据源设置也分为类似的两种:

1)静态数据源切换:

一般情况下,我们可以配置多个数据源,然后为每个数据源写一套对应的sessionFactory和dao层,我们称之为静态数据源配置,这样的好处是想调用那个数据源,直接调用dao层即可。但缺点也很明显,每个Dao层代码中写死了一个SessionFactory,这样日后如果再多一个数据源,还要改代码添加一个SessionFactory,显然这并不符合开闭原则。

2)动态数据源切换:

配置多个数据源,只对应一套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层里面动态切换数据源。

下面是我在实际项目中的一点应用(我是将事务控制和数据源切换都放在了service层,通过spring的aop设置先切换数据源再开启事务控制),相关配置分享到这里,大家共同探讨,欢迎技术交流(显示“xx”部分根据自己项目填写相应数据

 1、首先,要有数据库的相关配置文件jdbc.properties:

jdbc.rmi.driverClassName = com.csw.common.log4jdbc.CswDriverSpy
jdbc.rmi.url1 = jdbc:log4jdbc:oracle:thin:@192.168.x.x:1521:xx
jdbc.rmi.user1 = xxxx
jdbc.rmi.password1 = **** jdbc.rmi.url2 = jdbc:log4jdbc:oracle:thin:@192.168.x.x:1521:xx
jdbc.rmi.user2 = xxxx
jdbc.rmi.password2 = ****

 2、用spring管理数据源

<bean id="dataSource1" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.rmi.driverClassName}"/>
<property name="jdbcUrl" value="${jdbc.rmi.url1}"/>
<property name="username" value="${jdbc.rmi.user1}"/>
<property name="password" value="${jdbc.rmi.password1}"/> <property name="connectionTestQuery" value="SELECT 1 FROM DUAL"/>
<property name="maximumPoolSize" value="xx"/>
<property name="idleTimeout" value="xx"/>
<property name="maxLifetime" value="xx"/>
<property name="minimumIdle" value="xx"/>
<property name="poolName" value="ScmDatabasePool"/> <property name="dataSourceProperties">
<props>
<prop key="cachePrepStmts">true</prop>
<prop key="prepStmtCacheSize">xx</prop>
<prop key="prepStmtCacheSqlLimit">xx</prop>
<prop key="useServerPrepStmts">true</prop>
</props>
</property>
</bean> <bean id="dataSource2" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.rmi.driverClassName}"/>
<property name="jdbcUrl" value="${jdbc.rmi.url2}"/>
<property name="username" value="${jdbc.rmi.user2}"/>
<property name="password" value="${jdbc.rmi.password2}"/> <property name="connectionTestQuery" value="SELECT 1 FROM DUAL"/>
<property name="maximumPoolSize" value="xx"/>
<property name="idleTimeout" value="xx"/>
<property name="maxLifetime" value="xx"/>
<property name="minimumIdle" value="xx"/>
<property name="poolName" value="ScmDatabasePool"/> <property name="dataSourceProperties">
<props>
<prop key="cachePrepStmts">true</prop>
<prop key="prepStmtCacheSize">xx</prop>
<prop key="prepStmtCacheSqlLimit">xx</prop>
<prop key="useServerPrepStmts">true</prop>
</props>
</property>
</bean>

3、上面的数据源配置起来了,但是怎么样才能实现一个sessionFactory来管理两个源呢,需要一个动态的代理类,写一个RoutingDataSource类继承 AbstractRoutingDataSource ,并实现 determineCurrentLookupKey方法即可,AbstractRoutingDataSource是spring里的一个实现类,有兴趣的朋友可以研究一下他的源码。

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

/**
* @author
* @version 2019-08-02 12:36
*/
public class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceHolder.getDataSourceType();
}
}

 还要写一个数据源持有类,利用ThreadLocal解决线程安全问题

/**
* @author
* @version 2019-08-02 13:12
*/
public class DataSourceHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>(); /**
* @Description: 设置数据源类型
* @param dataSourceType 数据库类型
* @return void
* @throws
*/
public static void setDataSourceType(String dataSourceType) {
contextHolder.set(dataSourceType);
} /**
* @Description: 获取数据源类型
* @param
* @return String
* @throws
*/
public static String getDataSourceType() {
return contextHolder.get();
} /**
* @Description: 清除数据源类型
* @param
* @return void
* @throws
*/
public static void clearDataSourceType() {
contextHolder.remove();
} }

4、实现一个sessionFactory管理多个数据源

<bean id="dataSource" class="com.csw.purchase.config.RoutingDataSource">
<property name="targetDataSources">
<map key-type="java.lang.String">
<!--通过不同的key决定用哪个dataSource-->
<entry key="ds1" value-ref="dataSource1"/>
<entry key="ds2" value-ref="dataSource2"/>
</map>
</property>
<!-- 为指定数据源RoutingDataSource注入默认的数据源-->
<property name="defaultTargetDataSource" ref="dataSource1"/>
</bean>
<bean id="sqlSessionFactory" class="com.baomidou.mybatisplus.spring.MybatisSqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="configuration" ref="mybatisConfig"/>
<property name="typeAliasesPackage" value="com.csw.*.entity"/>
<property name="plugins">
<array>
<bean id="paginationInterceptor" class="com.baomidou.mybatisplus.plugins.PaginationInterceptor"/>
<bean id="optimisticLockerInterceptor" class="com.baomidou.mybatisplus.plugins.OptimisticLockerInterceptor"/>
</array>
</property>
<property name="globalConfig" ref="globalConfig"/>
</bean>

5、 建立一个数据源切面类,分别实现org.springframework.aop中的MethodBeforeAdvice、AfterReturningAdvice、ThrowsAdvice 三个接口,一开始我并未实现ThrowsAdvice 接口,后来在程序调试过程中发现数据源一旦切换到非默认数据源,目标方法(带有其他数据源注解的方法)抛出异常后将导致数据源切换失败,报talbe or view does not exist错误,究其原因应该是数据源持有类的DataSourceHolder中的线程ThreadLocal由于异常导致contextHolder.remove()未被执行,实现了ThrowsAdvice 接口后,可以完美解决这个问题,具体代码如下:

import org.aspectj.lang.annotation.Aspect;
import org.springframework.aop.AfterReturningAdvice;
import org.springframework.aop.MethodBeforeAdvice;
import org.springframework.aop.ThrowsAdvice;
import org.springframework.stereotype.Component; import java.lang.reflect.Method; /**
* @author
* @version 2019-08-02 13:15
*/ @Aspect
@Component
public class DataSourceAspect implements MethodBeforeAdvice, AfterReturningAdvice, ThrowsAdvice {
@Override
public void afterReturning(Object o, Method method, Object[] objects, Object o1) {
if(method.isAnnotationPresent(DataSource.class)) {
DataSourceHolder.clearDataSourceType();
System.out.println("**********************************数据源已移除*************************************");
}
} @Override
public void before(final Method method, final Object[] args, final Object target) {
if(method.isAnnotationPresent(DataSource.class)){
DataSource dataSource = method.getAnnotation(DataSource.class);
DataSourceHolder.setDataSourceType(dataSource.value());
System.out.println("*******************************数据源切换至:"+DataSourceHolder.getDataSourceType()+"**************************************");
}
} public void afterThrowing(final Method method, final Object[] args, final Object target, Exception e) {
if(method.isAnnotationPresent(DataSource.class)) {
DataSourceHolder.clearDataSourceType();
System.out.println("**********************************数据源已移除*************************************");
}
} }

 

6、建立数据源注解类,不加数据源注解的方法使用默认数据源,加了注解的使用注解对应的数据源

import org.springframework.stereotype.Component;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; /**
* @author
* @version 2019-08-02 13:14
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface DataSource {
String value() default "";
}

7、设置数据库事务切面和切换数据库切面执行的顺序,利用aop的order属性设置执行的顺序,这样实现了带事务管理的spring数据库动态切换

 <aop:config>
<aop:pointcut id="transactionPointcut" expression="execution(* com.csw.*.service.impl..*.*(..))"/>
<aop:advisor pointcut-ref="transactionPointcut" advice-ref="transactionAdvice" order="2"/>
<aop:advisor pointcut-ref="transactionPointcut" advice-ref="dataSourceAspect" order="1"/>
</aop:config>

8、测试,加了注解“ds2”的方法将用数据源ds2

@DataSource("ds2")
public Page<SupplierServiceOrder> listSupplierServiceOrderQuery(final Page<SupplierServiceOrder> page, final SupplierServiceOrder supplierServiceOrder) {
page.setRecords(baseMapper.listSupplierServiceOrderQuery(page, supplierServiceOrder));
return page;
}

目前上述配置实现了单个service调用单个方法调用单个数据源的带事务的数据源动态切换,如果该方法中需要调用另外的数据源,由于此时事务已经开启,按上述方法应该会导致另外的数据源切换失败,按上述配置,只能将此种情况按调用的数据源不同分开写在两个service方法中,然后再在controller层将结果合到一起。目前项目中暂未遇到这种情况,待遇到来验证。

带事务管理的spring数据库动态切换的更多相关文章

  1. Spring+Mybatis动态切换数据源

    功能需求是公司要做一个大的运营平台: 1.运营平台有自身的数据库,维护用户.角色.菜单.部分以及权限等基本功能. 2.运营平台还需要提供其他不同服务(服务A,服务B)的后台运营,服务A.服务B的数据库 ...

  2. 【Spring】Spring的事务管理 - 1、Spring事务管理概述(数据库事务、Spring事务管理的核心接口)

    Spring事务管理概述 文章目录 Spring事务管理概述 数据库事务 什么是Spring的事务管理? Spring对事务管理的支持 Spring事务管理的核心接口 Platform Transac ...

  3. 程序员笔记|Spring IoC、面向切面编程、事务管理等Spring基本概念详解

    一.Spring IoC 1.1 重要概念 1)控制反转(Inversion of control) 控制反转是一种通过描述(在java中通过xml或者注解)并通过第三方去产生或获取特定对象的方式. ...

  4. Spring AOP动态切换数据源

    现在稍微复杂一点的项目,一个数据库也可能搞不定,可能还涉及分布式事务什么的,不过由于现在我只是做一个接口集成的项目,所以分布式就先不用了,用Spring AOP来达到切换数据源,查询不同的数据库就可以 ...

  5. atomikos实现多数据源支持分布式事务管理(spring、tomcat、JTA)

    原文链接:http://iteye.blog.163.com/blog/static/1863080962012102945116222/   Atomikos TransactionsEssenti ...

  6. spring事务管理-Spring 源码系列(6)

    Spring事务抽象的是事务管理和事务策略.而实现则由各种资源方实现的.我们最常用的数据库实现:DataSourceTransactionManager 尝试阅读一下spring 的实现代码,由3个核 ...

  7. Java事务管理之Spring+Hibernate

    环境与版本 除了上一篇中的hibernate的相关lib 外 Java事务管理之Hibernate 还需要加入Spring的lib 包和如下的一些依赖包 org.aopallianceorg.aspe ...

  8. 框架源码系列十一:事务管理(Spring事务管理的特点、事务概念学习、Spring事务使用学习、Spring事务管理API学习、Spring事务源码学习)

    一.Spring事务管理的特点 Spring框架为事务管理提供一套统一的抽象,带来的好处有:1. 跨不同事务API的统一的编程模型,无论你使用的是jdbc.jta.jpa.hibernate.2. 支 ...

  9. spring测试junit事务管理及spring面向接口注入和实现类单独注入(无实现接口),实现类实现接口而实现类单独注入否则会报错。

    1.根据日志分析,spring junit默认是自动回滚,不对数据库做任何的操作. 18:16:57.648 [main] DEBUG o.s.j.d.DataSourceTransactionMan ...

随机推荐

  1. 编辑docker容器中的文件

    一般docker中没有VI或者其它相应的文本编辑器,为了写个东西安装个vi就可以解决问题,除此之外还有别的办法 登陆docker中找到需要编辑的文件的位置 sudo docker ps -a sudo ...

  2. SmartBinding与kbmMW#2

    前言 在之前的文章中,我介绍了SmartBinding作为Delphi的一个新的易于使用和智能的绑定框架.介绍了包括绑定对象,列表,常规数据和可视控件,以及如何使用导航器,所有这些都用代码做了演示. ...

  3. C# 获取当前执行DLL 所在路径

    有的时候,当前执行的DLL 和启动的EXE 所在路径并不一致,这时我们想要获得当前执行DLL 所在路径可以使用下面的方法. // Summary: // Gets the path or UNC lo ...

  4. JAVA语言程序设计课后习题----第四单元解析(仅供参考)

    1 本题水题,主要理解题目的意思即可,访问方法和修改方法可以通过快捷方式alt+insert选中你需要的成员变量即可 public class Person { public String name; ...

  5. 分享一个百万数量级的测试学习用的mysql数据集

    TEST_DB 带有集成测试套件的示例数据库,用于测试应用程序和数据库服务器 此存储库已从Launchpad迁移. 请参阅MySQL文档中的用法 它来自哪里 原始数据由西门子企业研究中心的Fushen ...

  6. STM32 ARM调试问题总结

    文章转载自:http://xfjane.spaces.eepw.com.cn/articles/article/item/77908 基于ADS的ARM调试有关问题总结 1.  在添加文件的过程中你可 ...

  7. rank 和 ROW_NUMBER 区别

    SELECT * , RANK() OVER ( PARTITION BY APP_NAME ORDER BY SETTING_NAME,SETTING_CODE ASC ) AS Rank FROM ...

  8. C# .NET 微信开发-------当微信服务器推送消息时如何接收处理

    最近一直在看微信,整整一个月了,看到现在说实话还有很多没看的,从前两周一点看不懂到现在单个功能的一步步实现,不知道这样的速度是否太慢了. 不过现在往下看还是有思路了,目前整个文档完成学习只有1/3左右 ...

  9. 决策树--CART树详解

    1.CART简介 CART是一棵二叉树,每一次分裂会产生两个子节点.CART树分为分类树和回归树. 分类树主要针对目标标量为分类变量,比如预测一个动物是否是哺乳动物. 回归树针对目标变量为连续值的情况 ...

  10. PHP类知识----静态属性和方法

    <?php class mycoach { public $name="陈培昌"; CONST hisage =; ; private $favorite = "喜 ...