动态切换数据源理论知识

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

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. mysql的锁机制,以及乐观锁,悲观锁,以及热点账户余额问题

    mysql的简单锁机制. myisam 1.只支持表级锁,所以经常更新的表结构不适宜用. 2.select也会产生锁表 innodb 1.支持事务,行级锁,表级锁,执行行级锁的前提是sql语句的索引有 ...

  2. Tomcat设置默认启动项目

    Tomcat设置默认启动项目 Tomcat设置默认启动项目,顾名思义,就是让可以在浏览器的地址栏中输入ip:8080,就能访问到我们的项目.具体操作如下:     1.打开tomcat的安装根目录,找 ...

  3. remote mounting from windows to linux

    8 Ways To Mount SMBfs (SAMBA FILE SYSTEM) In Linux. Sep 8, 2009 How to Mount smbfs (SAMBA file syste ...

  4. 微信小程序开发(一)创建一个小程序Hello World!

    开发微信小程序并不是很难,网上有很多小程序开发资料,尤其是微信官方的<小程序开发指南>最详细. 下面是我开发小程序的历程: 第一步,请前往https://mp.weixin.qq.com/ ...

  5. less 分页显示文件内容

    1.命令功能 less 是more的增强版,可以分页显示文件内容,而且less打开文件的速度要比vi,more更快.less支持搜索功能,显示行号. 2.语法格式 less  option  file ...

  6. html标签被div嵌套页面字体变大的解决办法

    html标签被div嵌套页面字体变大的解决办法 <div> <html> <head> <title></title> </head& ...

  7. .NET Core 3时代!如何使用DevExpress WPF创建应用

    DevExpress广泛应用于ECM企业内容管理. 成本管控.进程监督.生产调度,在企业/政务信息化管理中占据一席重要之地.通过DevExpress WPF Controls,您能创建有着强大互动功能 ...

  8. elementUI + vue 输入框只能输入正整数 不能输入字母 e 以及+ - 号

    <el-input :inline="true" v-model="dialogForm.closeTime" onKeypress="retu ...

  9. 箭头函数 -ES6

    1)函数参数只有一个:可以省略 ( ) var f = a => a     等同于 var f = function (a) { return a } 2)函数内部语句只有一个:可以省略 { ...

  10. shell小命令

    小括号的用途 cd ..; ls -l (cd ..; ls -l) 如果加了括号,则当前工作目录不发生改变 shell变量类型 环境变量 可以使用 echo  或者env 例如 env|grep 变 ...