Setting:

  绑定三个数据源(XA规范),将三个实例绑定到AbStractoutingDataSource的实例MultiDataSource(自定义的)对象中,mybatis  SqlSessionFactory数据源设定为MultiDataSource,DataSourceTransactionManager数据源绑定MultiDataSource,自定义注解,切面,就某个字段被crud操作涉及时,进入切面,计算目标数据库,动态切换数据库.不涉及事务时实现了动态切换,加了事务后..........

Question:

  1. AbstractRoutingDataSource是什么? 为什么它能动态切换数据库?

  2. 加上事务后还能动态切换数据库吗? Spring的事务是怎么开启的?

  3. 结合Mybatis 如何实现动态切换的同时开启分布式事务?

My opinion:

  For question 1:

  我们用一个类来继承AbstractRoutingDataSource,只需实现一个方法,那就是

  @Override
protected Object determineCurrentLookupKey() {
log.info("调用一次{}", MultiDataSourceHolder.getDataSourceKey());
String dataSourceKey = MultiDataSourceHolder.getDataSourceKey();
//初始化AbstractRoutingDataSource实现类时,dsRoutingSetProperties保存了多数据源名称
return dataSourceKey == null ? dsRoutingSetProperties.getDataSourceKeysMapping().get(0) : dataSourceKey;
}

     我们来看一下具体方法如何作用的

    //类实现了DataSource接口 getConnection() 调用此方法
protected DataSource determineTargetDataSource() {
Object lookupKey = this.determineCurrentLookupKey();
     //这里的作用的resolvedDataSources也是一个map,而我们实例化多数据源时设置的是名叫targetDataSource的map
DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
     dataSource = this.resolvedDefaultDataSource; } }
//我们来看另一个方法afterPropertiesSet,这个方法是实现了接口InitializingBean,在ioc实例化bean的时候,还未生成代理对象前会调用的方法,也就是还为注入到单例缓存池时 public void afterPropertiesSet() {
  ...
this.resolvedDataSources = new HashMap(this.targetDataSources.size());
     //java8添加的迭代方式
this.targetDataSources.forEach((key, value) -> {
Object lookupKey = this.resolveSpecifiedLookupKey(key);
       //实际上只有这里做了处理,如果map的值是String类型,用jndi的方式获取DataSource(我的技能树没点到这里,看不懂)
DataSource dataSource = this.resolveSpecifiedDataSource(value);
this.resolvedDataSources.put(lookupKey, dataSource);
});
if (this.defaultTargetDataSource != null) {
//这里的处理同上
this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
}

  看到这里,我们大概知道了AbstractRoutingDataSource其实就是对DataSource的包装,之前baidu搜索spring的设计模式时,大部分的博文都会说到Spring动态切换数据库就

用的是装饰模式,每每看到这里就是一脸懵(为了整明白才在个人Demo上加上动态数据库切换),做完之后那么问题来了,加了事务后的结果是怎么样呢?

  For question 2:

  看一下分别加了没加@Transactional  和 加了注解的 两个结果  null和dataSourcexx 是切面时存入线程变量的值

分别调用三次 和调用一次,这代表着一共产生了三个连接和一个连接.为什么加了事务之后只用了一次连接 ?  切面往线程变量里存了映射,但是没起作用?

花了三天才解决这个问题,这里我们从头撸起找原因

先看下事务的三个关键性接口
PlatformTransactionManager getTransaction/commit/rollback
TransactionStatus 事务运行状态 isNewTransaction()/hasSavepoint() 是否为新事务/是否有保存点
TransactionDefinition 事务属性 定义了大量关于隔离级别/传播行为的常量

我们知道@Transactional声明式事务是基于动态代理,事务也是以aop参与责任链形式调用

从头看  @EnableTransactionManagement,点开可以看到 @Import({TransactionManagementConfigurationSelector.class}),我们点进去瞅瞅

//ImportSelector接口需要实现的方法,作用是只要返回类的全限定名即可将Bean注入到容器内
protected String[] selectImports(AdviceMode adviceMode) {
switch(adviceMode) {
     //打个断点进去 发现走的是Proxy,默认走的就是Proxy,这里注册了两个bean,进去ProxyTransactionManagementConfiguration里看看,发现就是一个配置类,
//注册了三个bean,我们看一下TransactionInterceptor(拦截器,aop作用的实质)这个类

  进入TransactionInterceptor类后,感觉有用的就只有一个invoke方法,方法内回调父类的 invokeWithinTransaction()方法,我们跟进去看

    //方法很长,直接打一个端点在第一行,随便一个方法加上@Transactional,顺利进入
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass, TransactionAspectSupport.InvocationCallback invocation) throws Throwable {
     Object result;
if (...) {
...
} else {
        //方法直接进到这里了
TransactionAspectSupport.TransactionInfo txInfo = this.createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
try {
          //执行下一层拦截器链 直到执行目标方法
result = invocation.proceedWithInvocation();
protected TransactionAspectSupport.TransactionInfo createTransactionIfNecessary(@Nullable PlatformTransactionManager tm, @Nullable TransactionAttribute txAttr, final String joinpointIdentification) {
....
status = tm.getTransaction((TransactionDefinition)txAttr);
...
     //把TransactionStatus封装进TransactionInfo对象中,同时绑定到线程变量
return this.prepareTransactionInfo(tm, (TransactionAttribute)txAttr, joinpointIdentification, status);
public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException {
     Object transaction = this.doGetTransaction();
     //如果ConnnectionHolder不为null transactionActive为true 进入这里 ,因为已经确定存在事务了,进去就事务传播行为做判断,是否建立保存点等
if (this.isExistingTransaction(transaction)) {
return this.handleExistingTransaction((TransactionDefinition)definition, transaction, debugEnabled);
}
      .....
      else {
       //第一次 进入这里 不需要处理其他
AbstractPlatformTransactionManager.SuspendedResourcesHolder suspendedResources = this.suspend((Object)null);
protected Object doGetTransaction() {
DataSourceTransactionManager.DataSourceTransactionObject txObject = new DataSourceTransactionManager.DataSourceTransactionObject();
txObject.setSavepointAllowed(this.isNestedTransactionAllowed());
     //这里第一次(如果事务多次访问数据库)返回的为null,下文会看到具体过程
ConnectionHolder conHolder = (ConnectionHolder)TransactionSynchronizationManager.getResource(this.obtainDataSource());
txObject.setConnectionHolder(conHolder, false);
  //看到这里可以发现 transaction 对象就是DataSourceTransactionObject,继承自JdbcTransactionObjectSupport,有一个属性ConnectionHolder,用来干嘛的??
return txObject;
}
public static Object getResource(Object key) {
Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
     //这里的key就是 我们在声明TransactionManager注入的数据源 ,我这里就是MultiDataSource
Object value = doGetResource(actualKey);
private static Object doGetResource(Object actualKey) {
     //记住这个 resources 它是一个ThreadLocal<Map<Object, Object>>对象 ,第一进来肯定直接return null了
Map<Object, Object> map = (Map)resources.get();
if (map == null) {
return null;
} else {
       //这里试想 如果map不为null 通过绑定的数据源 我们能拿到什么 ?
Object value = map.get(actualKey);
if (value instanceof ResourceHolder && ((ResourceHolder)value).isVoid()) {
map.remove(actualKey);
if (map.isEmpty()) {
resources.remove();
}
value = null;
}
return value;
}
}

至此,函数回调 ,执行doBegin()方法

protected void doBegin(Object transaction, TransactionDefinition definition) {
DataSourceTransactionManager.DataSourceTransactionObject txObject = (DataSourceTransactionManager.DataSourceTransactionObject)transaction;
Connection con = null;
try {
        //前文发现,自身的ConnnectionHolder对象为null,这里就直接进入了
if (!txObject.hasConnectionHolder() || txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
Connection newCon = this.obtainDataSource().getConnection();
if (this.logger.isDebugEnabled()) {
this.logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
}
          //创建连接,重新设置ConnectionHolder对象
txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
}
txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
con = txObject.getConnectionHolder().getConnection();
Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
txObject.setPreviousIsolationLevel(previousIsolationLevel);
if (con.getAutoCommit()) {
txObject.setMustRestoreAutoCommit(true);
if (this.logger.isDebugEnabled()) {
this.logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
}
          //取消自动提交事务
con.setAutoCommit(false);
}
  // 这里影响后面 第二次进入 是否还会进入本方法
txObject.getConnectionHolder().setTransactionActive(true);
if (txObject.isNewConnectionHolder()) {
          //方法进入这里 bindResource方法是不是有点熟悉 ?前文有个方法叫doGetResource()
TransactionSynchronizationManager.bindResource(this.obtainDataSource(), txObject.getConnectionHolder());
}
public static void bindResource(Object key, Object value) throws IllegalStateException {
Map<Object, Object> map = (Map)resources.get();
    //之前的map为空,这下不为空了
if (map == null) {
map = new HashMap();
resources.set(map);
}
     //之前介绍的key 就是数据源实例对象,value就是刚传进来的ConnnectionHolder
Object oldValue = ((Map)map).put(actualKey, value);

看到这里大概清楚了为什么AbstractRoutingSource的方法为什么没有调用.大致总结下, 希望不会带偏读者

Spring创建事务时,每次都会

  1.创建一个TransactionAspectSupport.TransactionInfo对象,该对象中封装了PlatformTransactionManager,joinpointIdentification("被调用方法名"),TransactionStatus(事务状态,封装一个DataSourceTransactionObject对象,也是就是transaction对象,里面再包一层ConnectionHolder,保存数据库连接),同时关联一个本类对象OldTransactionInfo,

  2.获取transaction对象,先尝试从线程变量中获取ConnectionHolder对象,判定ConnectionHolder是否为null,如果是空值,则定义此次事务为新事务(newTransaction),从事务管理器中绑定的数据源中获取连接,绑定到ConnectionHolder对象中,并将holder存入线程变量.

    3,将OldTransaction对象设为自身,并存入线程变量,执行下一层aop或目标方法

  4.事务内再度调用带事务切面的代理对象时,步骤2从线程变量获取到Holder,进入handleExistingTransaction()方法,就事务传播行为决定是否挂起事务,设立保存点等,将事务属性newTransaction设为false

  ....

在发现这个问题时尝试了各种诡异举措来试图解决它:

  1.编程式事务TransactionTemplate  可以分为两个事务,但是这并没有什么意义..能不能使两个事务一起提交?

  2.自定义MyTemplate继承TransactionTemplate ,commit时 开启新线程,当两个运行都观测到目标结果(如redis)才提交,..可想而知,新的线程,存在原线程变量里的啥都成了孤岛, 卒..

  3.自定义事务管理器 / 自定义SqlSessionTemplate,将三个数据源都绑定到SqlSessionTemplate中,获取线程变量中的数据库名,返回对应的数据源 .. 然而不是不会用 就是根本就没被调用.. 这方面的知识还是太浅薄了..

For question 3:

  查看网上很多的案例,发现 有用JdbcTemplate控制的,有分多个SqlSessionFactory 用JTA控制的,让Dao层分开被SqlSessionFactory管理,确实可以实现跨库事务,但这一开始就决定了要落入哪个库.和我这个应用场景不一致..能否借鉴折中一下呢?

@Configuration
@MapperScan(value = "com.zuan.cinema.mapper",sqlSessionFactoryRef = "sqlSessionFactory00")
@MapperScan(value = "com.zuan.cinema.mapper.m",sqlSessionFactoryRef = "sqlSessionFactory01")
public class DataSourceConfiguration {
   @Bean
@Primary
public SqlSessionFactory sqlSessionFactory00(@Qualifier("dataSource00") DataSource dataSource) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mybatis/mapper/*.xml"));
Interceptor interceptor = new PageInterceptor();
sqlSessionFactoryBean.setPlugins(new Interceptor[]{interceptor});
return sqlSessionFactoryBean.getObject();
} @Bean
public SqlSessionFactory sqlSessionFactory01(@Qualifier("multiDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mybatis/mapper/m/*.xml"));
Interceptor interceptor = new PageInterceptor();
sqlSessionFactoryBean.setPlugins(new Interceptor[]{interceptor});
return sqlSessionFactoryBean.getObject();
}
}

  因为crud操作时,只会受一个字段影响决定使用哪个数据库的连接,那么就可以定义两个sqlSessionFatory,一个管理普通mapper,引用普通数据源,另一个管理直接关联字段的mapper,应用MultiDataSource,这样进入事务后必然通过MultiDataSource动态选择数据库.这里只有用JTA事务管理才能实现切换,它和DataSourceTransactionManager实现机制不一样,建立的连接也不样.更多的我看不懂...

Epilog:

  结果只在个人Demo场景下测试成功,没有经历过生产环境的洗礼.并且上面的推断也出自未经过生产环境的人之口,请自行判断...

SSM使用AbstractRoutingDataSource后究竟如何解决跨库事务的更多相关文章

  1. 【Java EE 学习 19】【使用过滤器实现全站压缩】【使用ThreadLocal模式解决跨DAO事务回滚问题】

    一.使用过滤器实现全站压缩 1.目标:对网站的所有JSP页面进行页面压缩,减少用户流量的使用.但是对图片和视频不进行压缩,因为图片和视频的压缩率很小,而且处理所需要的服务器资源很大. 2.实现原理: ...

  2. Spring3.0+Hibernate+Atomikos集成构建JTA的分布式事务--解决多数据源跨库事务

    一.概念 分布式事务分布式事务是指事务的参与者.支持事务的服务器.资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上.简言之,同时操作多个数据库保持事务的统一,达到跨库事务的效果. JTA ...

  3. mysql 跨库事务XA

    前一段时间在工作中遇到了跨库事务问题,后来在网上查询了一下,现在做一下整理和总结. 1.首先要确保mysql开启XA事务支持 SHOW VARIABLES LIKE '%XA%' 如果innodb_s ...

  4. vue项目打包本地后通过nginx解决跨域

    前言 有时候我们打包好vue项目让后端人员部署项目时可能会有小插曲,为了不麻烦后端人员和避免尴尬,最好的办法就是在本地自己先测一下,而在本地运行打包后的项目会遇到接口跨域的问题.我平时经常用的方法就是 ...

  5. java跨库事务Atomikos

    1:引入额外的jar <dependency> <groupId>com.atomikos</groupId> <artifactId>transact ...

  6. mysql跨库复制: replicate_wild_do_table和replicate-wild-ignore-table

    使用replicate_do_db和replicate_ignore_db时有一个隐患,跨库更新时会出错. 如设置 replicate_do_db=testuse mysql;update test. ...

  7. Ajax 调用webservice 解决跨域请求和发布到服务器后本地调用成功外网失败的问题

        webservice 代码 /// <summary> /// MESService 的摘要说明 /// </summary> [WebService(Namespac ...

  8. 使用 Nginx 部署前后端分离项目,解决跨域问题

    前后端分离这个问题其实松哥和大家聊过很多了,上周松哥把自己的两个开源项目部署在服务器上以帮助大家可以快速在线预览(喜大普奔,两个开源的 Spring Boot + Vue 前后端分离项目可以在线体验了 ...

  9. 后台访问 JS解决跨域问题

    今天看了看以前做的一个小项目(其实就是一个页面),分享一下当时解决跨域问题的: 背景:公司把项目部署在多台服务器上,防止一台服务器崩溃后,其他的可以继续访问,对应本公司来说,某台服务器出问题后,技术人 ...

随机推荐

  1. 使用visual studio 2013读取.mat文件

    现在有一个T.mat 文件需要在c++中处理然后以.mat 或是.txt形式返回 T.mat中存储了十个cell,每个cell中会有一个不等长的数组 1.以下是相关配置过程: 参考:http://we ...

  2. AS中使用真机调试时出现解析错误的问题

    时间:2019/12/8 今天使用usb调试程序时手机上出现了解析错误的问题,其实这个问题很简单,主要可能是你想要调试的程序的最低版本号大于你手机的安卓版本号的原因,只需要修改下面这个地方: buil ...

  3. 位运算在状态压缩DP中的应用

    一.判断一个数字X的i位是不是1 方法:   << (i-)) & x > )  原理: 1左移(i-1)位,相当于制造了一个就i位上是1其他位都是0的一个二进制数.将这个数 ...

  4. js dom一些操作,记录一下自己写的没有意义,可以简略翻过 第八章

    第八章,一些dom操作,和几个常用的函数 var s= document.getElementById("new"); console.log(s.length); var a= ...

  5. BZOJ 3343 教主的魔法(分块)

    题意: 有一个1e6的数组,t次操作:将[l,r]内的值增加w,或者查询[l,r]内的值大于等于add的 思路: 分块,块大小为sqrt(n),每次只需要暴力头尾两块,中间的整块打标记, 对于查询查操 ...

  6. To be contine ,NW NMM backup sqlserver failed.

    Last time, we talk about separate under one cluster backup into two diffetent AG backup. Does it wor ...

  7. Python:自动化上传OSS

    简介 最近在学习Python,为之庞大的第三方库感到震撼.今天分享一个Python 自动化脚本,功能是将H5静态资源上传到OSS,以方便实现CDN加速,我将其放在Jenkins自动发布中使用.该脚本不 ...

  8. 一文读懂什么是一致性hash算法

    Hash,一般翻译做散列.杂凑,或音译为哈希,是把任意长度的输入通过散列算法变换成固定长度的输出,该输出就是散列值.这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入值的空间,不同的输入可能会 ...

  9. GDAL利用地理坐标读取图像像元值

    最近的一个项目需要在电子海图中下载已知水深点,导出点的地理坐标(经纬度).然后在arcgis中打开这些地理坐标输出为shp,利用GDAL读取不同波段的点对应的像元值,从而构建水深和像元值的对应关系. ...

  10. vue路由核心要点(vue-router)

    目录 目录 1.vue-router 是什么? 2.如何使用v-router? 3.vue-router跳转和传参 4.vue-router实现的原理 两种模式 5.vue-router 有哪几种导航 ...