一、背景

最近项目持续改造,然后把SpringMVC换成了SpringWebflux,然后把Mybatis换成了R2dbc。中间没有遇到什么问题,一切都那么的美滋滋,直到最近一个新需求的出现,打破了往日的宁静。

在对需求分析了一番后,需要引入新的数据源,那就是MongoDb。然后出现了MongoDb、Mysql两种数据源,然后原来好好的事物操作就芭比Q(完蛋)了。细细来分析一下原因与解决方法。

题外话:在本地测试的时候强烈建议用虚拟机+Docker来安装MySql与MongoDb,不然Mac直连docker真的麻烦啊!!~

SpringBoot 版本号: 2.6.10, (本文基于已经会在项目中使用R2DBC与MongoDb)

二、武松打虎

2.1 单独solo Mysql

我们创建了一个测试库r2dbc_test,里面有一个user表。

# 创建测试库
create database r2dbc_test; # 创建表
create table r2dbc_test.user(
id int auto_increment primary key ,
name varchar(12)
);

2.1.1 项目引入R2dbc

略..给出链接,如果感兴趣可以看看,Spring Data R2DBC,(实在太多,这个时间点懒得写了,后面有时间再补一下),

2.1.2 测试代码

创建表对结构对应实体类:user

@Data
@Table("user")
@NoArgsConstructor
@AllArgsConstructor
public class User implements Persistable<Integer> {
@Id
private Integer id;
private String name; @Override
public boolean isNew() {
return true;
}
}

这里面有个坑点,那就是为什么实现org.springframework.data.domain.Persistable这个接口呢,先卖个关子,看完Repository后在描述哈。

Repository如下代码所示。

/**
* <br>User Repository</br>
*
* @author fattyca1@qq.com
* @since 2022/8/26
*/
@Repository
public interface UserR2dbcRepository extends R2dbcRepository<User, Integer> { }

我们直接使用了Spring提供好的org.springframework.data.r2dbc.repository.R2dbcRepository,里面有一些基础的实现类。我们在测试的时候使用了org.springframework.data.repository.reactive.ReactiveCrudRepository#save()方法,这个方法会去判断这个实体对象是不是new object,如果不是,则会去Update。而判断的方法则是org.springframework.data.domain.Persistable#isNew()方法。所以这就是我们为啥要实现这个接口。

接着写一个简单测试的Controller,代码如下所示。

@RestController
@EnableR2dbcRepositories
public class TransactionController {
@Autowired
private UserR2dbcRepository repository;
@Autowired
private TransactionalOperator operator; // 根据seed当做初始ID,初始化数据库对象, 便于测试
@RequestMapping("/r2dbc/init")
public Flux<User> init(Integer seed) {
Flux<User> userFlux = Flux.range(seed, 5).map(id -> new User(id,"name" + id))
.flatMap(repository::save);
return userFlux;
} // 先删除一条记录, 然后在添加一条记录
@RequestMapping("/r2dbc/delete")
public Mono<User> delete(Integer id1, Integer id2) {
Mono<Void> id1Mono = repository.deleteById(id1);
Mono<User> id2Mono = repository.save(new User(id2, "name" + id2));
return id1Mono.then(id2Mono).as(operator::transactional);
}
}

不要纠结没有service啥的哈,我们仅仅为了测试哈。两个方法

  • 方法一:init, 用seed当做起始Id, 然后在数据库生成数据存储起来
  • 方法二:delete, 先删处一条数据,然后在插入一条已存在的数据,通过数据库异常来回滚数据。

我们调用init方法,生成数据id=1和id=100以后的数据,如下图所示。

为了查看我们是不是插入成功,我们查一下数据库看看。结果如下图。

数据看起来是没问题的哈,是我们想要的,从1-5, 100-105

2.1.3 测试事务

数据已经准备好了,我们来进行事务测试,看看现在只有R2DBC的时候,事务是否生效。

我们来删除id=1,然后保存id=100的情况试一下看看。结果如图所示。

通过日志,我们看到结果的确是我们想要的,当id2=100的时候,抛出了Dulicate entry异常, 那我们在查询一下数据库,看看数据库的数据是否有删除掉。

结果还是用图展示。

我们通过查看数据库的查询记录,发现id=1数据没有删除。那也说明了事务是生效的,在正常情况下,发生异常不会提交事务。

2.2 引入MongoDb

略...感兴趣的老哥参考Spring Data MongoDb引入MongoDB

2.2.1 开启MongoDb事务

官方文档中有这样一句话:

Unless you specify a MongoTransactionManager within your application context, transaction support is DISABLED. You can use setSessionSynchronization(ALWAYS) to participate in ongoing non-native MongoDB transactions.

需要手动指定MongoTransactionManager,否则不可用。 引入事务,参考文档,需要如下代码。

@Bean
MongoTransactionManager transactionManager(MongoDatabaseFactory dbFactory) {
return new MongoTransactionManager(dbFactory);
}

我们按照文档指示,在项目中添加了如下代码。因为我们用的是Webflux,所以我们创建的是Reactive的。

@EnableReactiveMongoRepositories
@Configuration
public class MongoConfig {
@Bean
ReactiveMongoTransactionManager transactionManager(ReactiveMongoDatabaseFactory dbFactory) {
return new ReactiveMongoTransactionManager(dbFactory);
}
}

这样,我们MongoDB的事物也搞定了,直接美滋滋,上手开干CRUD。

2.2.2 再来一次----测试数据删除

我们引入了新的数据源,本该美滋滋的,但是,问题也来了。我们在来进行一次数据删除操作。这次删除,我们修改一下Id,删除id=2和添加id=102的。测试如下图所示。

我们再一次看到了同样的情况,抛出了异常Duplicate entry,是我们预期的结果。那我们接着看看数据库的数据。如下图所示。

这个时候我们在查询数据,发现id=2的数据已经被删除了。这次事务没有回滚! 真是F了个K,啥情况呢?我们得一探究竟。

三、智取谜底

我们带着问题来找原因,现在事务失效了,项目能起来,没有报错。那么最有的可能那就是TransactionalOperator失效了,TransactionalOperator是Spring帮我们初始化的,我们要找问题,那就得要看看这个TransactionalOperator是如何初始化的了

3.1 看源码找原因

3.1.1 从根本入手

我们直接从TransactionalOperator代码进入,发现其需要传入ReactiveTransactionManager,部分代码如下。

final class TransactionalOperatorImpl implements TransactionalOperator {

	private final ReactiveTransactionManager transactionManager;
private final TransactionDefinition transactionDefinition; /**
* Construct a new TransactionTemplate using the given transaction manager,
* taking its default settings from the given transaction definition.
* @param transactionManager the transaction management strategy to be used
* @param transactionDefinition the transaction definition to copy the
* default settings from. Local properties can still be set to change values.
*/
TransactionalOperatorImpl(ReactiveTransactionManager transactionManager, TransactionDefinition transactionDefinition) { this.transactionManager = transactionManager;
this.transactionDefinition = transactionDefinition;
}
}

按照一般逻辑来说,事务是放在TransactionManager中来管理的,这个符合我们的预期,我们接着看看TransactionManager的实现类有哪些。经过查看,发现有R2dbcTransactionManager实现。如下图所示。

![TransactionManager实现类]](https://img2022.cnblogs.com/blog/1495071/202208/1495071-20220827211141944-495282422.jpg)

3.1.2 按照猜想继续

我们找到了R2dbcTransactionManager,那我们就有两个思路。

1、查看其实现方式,有哪些需要我们关注的,哪些因素是可能造成事务不生效。

2、启动方式。因为R2dbcTransactionManager初始化是交由SpringBoot实现,那会不会有什么特别之处。

3.1.2.1 思路1

我们打开R2dbcTransactionManager代码,发现其实现没有特别之处。部分代码如下。

public class R2dbcTransactionManager extends AbstractReactiveTransactionManager implements InitializingBean {

	@Nullable
private ConnectionFactory connectionFactory;
/**
* Create a new {@code R2dbcTransactionManager} instance.
* A ConnectionFactory has to be set to be able to use it.
* @see #setConnectionFactory
*/
public R2dbcTransactionManager() {}
/**
* Create a new {@code R2dbcTransactionManager} instance.
* @param connectionFactory the R2DBC ConnectionFactory to manage transactions for
*/
public R2dbcTransactionManager(ConnectionFactory connectionFactory) {
this();
setConnectionFactory(connectionFactory);
afterPropertiesSet();
}
}

可以看到,无参初始化可以不需要ConnectionFactory,也可以传入ConnectionFactory进行初始化。 也没有什么特别之处。

3.1.2.2 思路2

我们看完其实现,并没有特别之处,那就看它初始化有什么特别的地方。Double Shift 来一波,我们看到了有AutoConfiguration,来让我们瞧一瞧。

我们点进去瞧一瞧,便发现了端倪,嘴上一句 原来如此 蹦了出来。部分代码如下。

public class R2dbcTransactionManagerAutoConfiguration {
@Bean
@ConditionalOnMissingBean(ReactiveTransactionManager.class)
public R2dbcTransactionManager connectionFactoryTransactionManager(ConnectionFactory connectionFactory) {
return new R2dbcTransactionManager(connectionFactory);
}
}

我们看到,其初始化的时候,采用了ConditionalOnMissingBean,只有在没有ReactiveTransactionManager的时候才会初始化。但是我们在初始化MongoDB事务的时候,已经初始化过ReactiveTransactionManager了啊!赶紧看看ReactiveMongoTransactionManager。

打开ReactiveMongoTransactionManager代码,果然如此。代码如下。

public class ReactiveMongoTransactionManager extends AbstractReactiveTransactionManager implements InitializingBean {
// ...略
}

AbstractReactiveTransactionManager这个不就是ReactiveTransactionManager嘛, 已经初始化过一次了,所以导致R2dbcTransactionManager无法进行初始化,所以TransactionalOperatorImpl里面传入的不是R2dbcTransactionManager,那肯定对mysql无法失误操作了啊。

3.1.3 怎么办?

至此,我们已经找到原因了,但是,这也紧紧是猜想。我们还是得分2步骤来啊!!

  • 1、针对问题,提出具体的解决方案,并实现
  • 2、针对实现的方案进行验证
3.1.3.1 解决方案

我们知道事务没有实现的原因是R2dbcTransactionManager没有初始化,然后再TransactionalOperatorImpl种注入的不是R2dbcTransactionManager,那么我们就自己动手初始化Bean。

我们创建2个对象,分别为MongoConfig和R2dbcConfig,代码如下所示。

R2dbcConfig:

/**
* <br>r2dbc 配置</br>
*
* @author fattyca1@qq.com
* @since 2022/8/27
*/
@EnableR2dbcRepositories
@Configuration
public class R2dbcConfig { @Bean("r2dbcTransactionManager")
public R2dbcTransactionManager transactionManager(ConnectionFactory pool) {
return new R2dbcTransactionManager(pool);
} @Bean("r2dbcTransactionalOperator")
public TransactionalOperator transactionalOperator(R2dbcTransactionManager transactionManager){
return TransactionalOperator.create(transactionManager);
}
}

MongoConfig:

/**
* <br>mongo transaction manager</br>
*
* @author fattyca1@qq.com
* @since 2022/8/27
*/
@EnableReactiveMongoRepositories
@Configuration
public class MongoConfig { @Bean("mongoTransactionManager")
public ReactiveMongoTransactionManager transactionManager(ReactiveMongoDatabaseFactory dbFactory) {
return new ReactiveMongoTransactionManager(dbFactory);
} @Bean("mongoTransactionalOperator")
public TransactionalOperator transactionalOperator(ReactiveMongoTransactionManager transactionManager){
return TransactionalOperator.create(transactionManager);
}
}

我们通过别名的方式,创建两个TransactionalOperator,这样就可以解决R2bdc无法自动创建TransactionManager的问题。

3.1.3.2 验证

我们在Controller中的TransactionalOperator指定名称。代码如下所示。

@RestController
public class TransactionController {
@Autowired
private UserR2dbcRepository repository; @Autowired
@Qualifier("r2dbcTransactionalOperator") // 在这指定使用哪个operator
private TransactionalOperator operator;
// ... 略
}

指定了具体的名称,我们就可以接着在来测试一次。这次我们删除Id=3,然后添加id=103的数据试试看。测试过程如下图。

还是和我们刚一下,出现了Duplicate entry的问题。我们要关注事物是否回滚。

接下来就是激动人心的时刻,我们直接查库,看看事务是否回滚了。结果如下图所示。

哇喔!棒!我们看到,数据库查询出来的结果中还是包含了Id=3的数据,那完全说明了事务回滚了!

至此我们的问题算是完全解决了,舒坦!(心里长舒一口气,解决问题就这么简单?)

3.2 偷鸡

看了这么多,我们都是手动,一步步验证结果的,哪有没有快捷的方式呢?说到这,那肯定是有的。

在使用R2dbc的时候,我们其实是没有添加日志的。我们可以打开日志。可以看到操作是记录了完整的日志。我们添加日志配置(log配置文件自己添加一下)。

logging.level.org.springframework.r2dbc=debug

3.2.1 再次验证

添加完日志,我们在执行一下删除id=3,添加id=104的操作,看看日志记录了什么。贴出来测试结果。

我们可以看到,日志中清晰的记录着,创建事务,回滚事务!完全验证了我们的操作方案是对的,NO爬不浪~!

上述的所有操作,都可以通过日志验证,我就不一步步验证,大家可以自己试验一下~

四、总结

在使用新东西的时候,还是要多实验,验证结果!

遇到问题,不要慌,一步步来,就是干!

如有问题,欢迎指正,交流。

(WebFlux)003、多数据源R2dbc事务失效分析的更多相关文章

  1. spring多数据源分布式事务的分析与解决方案

    一.概述 1.业务背景 对老系统进行重构合并,导致新系统需要同时对3个数据库进行管理.由于出现跨库业务,需要实现分布式事务. 2.开发环境 spring框架版本  4.3.10.RELEASE 持久层 ...

  2. spring事务失效情况分析

    详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt113 <!--[if !supportLists]-->一.&l ...

  3. Spring事务Transactional和动态代理(三)-事务失效的场景

    系列文章索引: Spring事务Transactional和动态代理(一)-JDK代理实现 Spring事务Transactional和动态代理(二)-cglib动态代理 Spring事务Transa ...

  4. Spring事务深入剖析--spring事务失效的原因

    之前我们讲的分布式事务的调用都是在一个service中的事务方法,去调用另外一个service中的业务方法, 如果在一个sevice中存在两个分布式事务方法,在一个seivice中两个事务方法相互嵌套 ...

  5. spring声明式事务 同一类内方法调用事务失效

    只要避开Spring目前的AOP实现上的限制,要么都声明要事务,要么分开成两个类,要么直接在方法里使用编程式事务 [问题] Spring的声明式事务,我想就不用多介绍了吧,一句话“自从用了Spring ...

  6. spring声明式事务 同一类内方法调用事务失效(转)

    原文 https://blog.csdn.net/jiesa/article/details/53438342 [问题] Spring的声明式事务,我想就不用多介绍了吧,一句话“自从用了Spring ...

  7. Spring事务实现分析

    一.Spring声明式事务用法 1.在spring配置文件中配置事务管理器 <bean id="baseDataSource" class="com.alibaba ...

  8. Spring事务失效的 8 大原因,这次可以吊打面试官了!

    今天再来一篇<吊打面试官>系列,这次真的要吊打了,哈哈!(看往期吊打系列请在后台回复:吊打,我会陆续更新--) 前几天栈长不是发了一篇文章,里面有一个关于事务失效的问题: 用 Spring ...

  9. Spring事务原理分析-部分二

    Spring事务原理分析-部分二 说明:这是我在蚂蚁课堂学习了余老师Spring手写框架的课程的一些笔记,部分代码代码会用到余老师的课件代码.这不是广告,是我听了之后觉得很好. 课堂链接:Spring ...

随机推荐

  1. 【spring源码系列】之【FactoryBean类型的接口】

    1.概述 目前我们知道,spring创建bean有多种方式,比如xml方式创建,比如@Component,@Service,@Controler,@Repository注解创建,比如@Autowire ...

  2. ssh打通

    打通ssh https://www.cnblogs.com/yolanda-lee/p/4975453.html

  3. SAP BPC 开发日记

    1.获取维度模型的方法1 DATA:i_appset_id TYPE uj_appset_id,     i_appl_id   TYPE uj_appl_id.i_appset_id = 'SINO ...

  4. HDFS存储目录分析

    一.介绍 HDFS metadata以树状结构存储整个HDFS上的文件和目录,以及相应的权限.配额和副本因子(replication factor)等.本文基于Hadoop2.6版本介绍HDFS Na ...

  5. XXXX系统测试计划

    XXXX系统测试计划 目录 XXXX系统测试计划 目标 概述 项目背景 适用范围 组织形式 组织架构图 角色及职责 测试工作分工 团队协作 测试对象 应测试特性 不被测试特性 测试任务安排 系统测试任 ...

  6. jdk8 hashmap 链表resize 源码分析

    重点看这部分代码 for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { old ...

  7. 从Wannacry到WannaRen:螣龙安科带你深度分析勒索病毒原理

    从Wannacry到WannaRen:螣龙安科2020年4月7日,360CERT监测发现网络上出现一款新型勒索病毒wannaRen,该勒索病毒会加密windows系统中几乎所有的文件,并且以.Wann ...

  8. [Java学习笔记] Java异常机制(也许是全网最独特视角)

    Java 异常机制(也许是全网最独特视角) 一.Java中的"异常"指什么 什么是异常 一句话简单理解:异常是程序运行中的一些异常或者错误. (纯字面意思) Error类 和 Ex ...

  9. docker部署练习

    三个部署任务 docker部署nginx docker pull nginx #拉取nginx镜像 docker images #检查拉取的镜像 docker run -d -p 3344:80 -- ...

  10. NOI / 2.1基本算法之枚举 1749:数字方格

    描述: 如上图,有3个方格,每个方格里面都有一个整数a1,a2,a3.已知0 <= a1, a2, a3 <= n,而且a1 + a2是2的倍数,a2 + a3是3的倍数, a1 + a2 ...