Spring事务监听,为什么会出现事务失效?

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: Spring 在 4.2 版本之后提供了@TransactionlEventListener 注解,可以很方便地在事务提交后做一些处理,但是如果使用不当,或者没有正确理解其背后的运行逻辑,很容易踩坑甚至导致线上故障。之前工作中就遇到了一个问题,在事务监听时,做了一些事务操作,但是这个事务并没有生效。今天我们就来深入了解一下,这个问题是怎么产生的,又该如何解决。

Spring 在 4.2 版本之后提供了@TransactionlEventListener 注解,可以很方便地在事务提交后做一些处理,但是如果使用不当,或者没有正确理解其背后的运行逻辑,很容易踩坑甚至导致线上故障。

之前工作中就遇到了一个问题,在事务监听时,做了一些事务操作,但是这个事务并没有生效。

今天我们就来深入了解一下,这个问题是怎么产生的,又该如何解决。

问题复现

我们来模拟一个很简单的场景:创建订单的时候会发布“订单已注册”的事件,在事件监听里保存操作记录,再发布“操作记录已保存”的事件,最后在这个事件监听里做一些逻辑。

以下代码中省略了一些不重要的实现。

首先是 OrderService,createOrder() 方法里保存订单记录,发布“订单已注册”的事件:

public class OrderService {
    @Transactional
    public void createOrder() {
        String orderNo = "test_no";
        Order order = new Order(orderNo);
        orderRepository.save(order);
        log.info("publish OrderCreatedEvent");
        applicationContext.publishEvent(new OrderCreatedEvent(orderNo));
    }
}

“订单已注册”的事件监听里,调用
operationService.saveOperation():

public class OrderCreatedEventListener {
    @TransactionalEventListener
    public void handle(OrderCreatedEvent event) {
        log.info("handle OrderCreatedEvent : " + event.getOrderNo());
        operationService.saveOperation(event.getOrderNo(), "创建订单");
    }
}


OperationService.saveOperation(),保存操作记录,并发布“操作记录已保存”的事件:

public class OperationService {
    @Transactional
    public void saveOperation(String orderNo, String info) {
        Operation operation = new Operation(orderNo, info);
        operationRepository.save(operation);
        log.info("publish OperationSavedEvent");
        applicationContext.publishEvent(new OperationSavedEvent(orderNo, info));
    }
}

“操作记录已保存”的事件监听里,打印一下日志,代替后续操作:

public class OperationSavedEventListener {
    @TransactionalEventListener
    public void handle(OperationSavedEvent event) {
        log.info("handle OperationSavedEvent : " + event.getOrderNo());
    }
}

开始测试,调用一下 orderService.createOrder() 方法,看一下日志打印:

Hibernate: insert into order_entity (id, order_no) values (null, ?)
INFO c.l.s.service.OrderService    : publish OrderCreatedEvent
INFO c.l.s.event.OrderCreatedEventListener    : handle OrderCreatedEvent : test_no
INFO c.l.s.service.OperationService: publish OperationSavedEvent

奇怪的事情发生了!数据库里只写入了订单数据,并没有写入操作记录,而且发布了 OperationSavedEvent 事件后,监听回调没有执行。

问题排查

先翻阅一下官方文档,在 事务事件 章节内,有这么一段提示:

网络异常,图片无法展示
|

最后一句话的意思是:在事务事件监听内,已经没有可供加入的事务。

回顾一下上面的问题代码,OrderService.createOrder() 是一个事务方法,这个事务提交后,触发了
OperationSavedEventListener,而在这个监听方法里,OperationService.saveOperation() 也是一个事务方法,传播类型为默认,即会加入当前事务。

但是在执行 saveOperation() 时,前面的事务已经完成了提交,所以没办法加入,导致操作记录保的事务没有真正执行。又因为操作记录保存的事务没有执行,所以没有触发
OperationSavedEventListener。

哦~大概明白了问题所在,我们进入 Spring 源码看一看是不是真的如此。

首先将 JPA 的日志级别调整为 debug

logging.level.org.springframework.orm.jpa=debug

再运行一下,看看日志:

DEBUG o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [co.lilpilot.springtestfield.service.OrderService.createOrder]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
DEBUG o.s.orm.jpa.JpaTransactionManager        : Opened new EntityManager [SessionImpl(1115296438<open>)] for JPA transaction
DEBUG o.s.orm.jpa.JpaTransactionManager        : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@fe87ddd]
DEBUG o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(1115296438<open>)] for JPA transaction
DEBUG o.s.orm.jpa.JpaTransactionManager        : Participating in existing transaction
Hibernate: insert into order_entity (id, order_no) values (null, ?)
INFO c.l.s.service.OrderService    : publish OrderCreatedEvent
DEBUG o.s.orm.jpa.JpaTransactionManager        : Initiating transaction commit
DEBUG o.s.orm.jpa.JpaTransactionManager        : Committing JPA transaction on EntityManager [SessionImpl(1115296438<open>)]
INFO c.l.s.event.OrderCreatedEventListener    : handle OrderCreatedEvent : test_no
DEBUG o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(1115296438<open>)] for JPA transaction
DEBUG o.s.orm.jpa.JpaTransactionManager        : Participating in existing transaction
DEBUG o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(1115296438<open>)] for JPA transaction
DEBUG o.s.orm.jpa.JpaTransactionManager        : Participating in existing transaction
INFO c.l.s.service.OperationService: publish OperationSavedEvent
DEBUG o.s.orm.jpa.JpaTransactionManager        : Cannot register Spring after-completion synchronization with existing transaction - processing Spring after-completion callbacks immediately, with outcome status 'unknown'
DEBUG o.s.orm.jpa.JpaTransactionManager        : Closing JPA EntityManager [SessionImpl(1115296438<open>)] after transaction

注意,出现了一行日志提示:“Cannot register Spring after-completion synchronization with existing transaction - processing Spring after-completion callbacks immediately, with outcome status 'unknown'”。

顺藤摸瓜进入 JpaTransactionManager 类,其实这一行日志的打印是在它的抽象父类中,即
AbstractPlatformTransactionManager.registerAfterCompletionWithExistingTransaction()

网络异常,图片无法展示
|

可以看到这里指定了事务状态为 STATUS_UNKNOWN,所以后续的回调逻辑里不再执行事务操作了。这个方法是在
AbstractPlatformTransactionManager.triggerAfterCompletion() 内被调用的:

网络异常,图片无法展示
|

在这里判断了事务的状态,此时我们的事务状态为有事务,但不是一个新事务,所以进了第二个判断分支。而触发的地方,就是
AbstractPlatformTransactionManager.processCommit(),也就是 Spring 处理事务提交的地方:

private void processCommit(DefaultTransactionStatus status) throws TransactionException {
    try {
        //... 省略 doCommit 相关逻辑
        try {
            triggerAfterCommit(status);
        }
        finally {
            // ①
            triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED);
        }
    }
    finally {
        // ②
        cleanupAfterCompletion(status);
    }
}

在 commit 逻辑处理完成后,即标识①的位置,触发了事务提交后的回调。

看到这里,问题已经很清楚了,Spring 在事务提交后,会触发后续回调逻辑,但是如果回调逻辑里也存在事务方法,却又不是一个新事务时,这个妄想加入的事务不会被提交。

问题解决

其实明白了问题,解决方案自然也很简单,只需要调整一下事务的传播类型,把保存操作记录的方法,标示为一个新的事务就好了:

public class OperationService {
    @Transactional(Transactional.TxType.REQUIRES_NEW)
    public void saveOperation(String orderNo, String info) {
        Operation operation = new Operation(orderNo, info);
        operationRepository.save(operation);
        log.info("publish OperationSavedEvent");
        applicationContext.publishEvent(new OperationSavedEvent(orderNo, info));
    }
}

这样子,操作记录的保存就能写入数据库,而且也能触发后续的事件监听。

One More Thing

且慢,我们再回想一下,Spring 的事件监听机制,其实是基于观察者模式的同步回调,而事务事件的监听同理,也是在事务提交后,获取事务同步注册器中已经注册了的回调,再同步执行。

刚才分析了
AbstractPlatformTransactionManager.processCommit(),触发回调方法 triggerAfterCompletion() 之后,还有最后一步操作 cleanupAfterCompletion(),即标识②所在的位置。

而在这一步中,才会关闭数据库的连接。

你是不是意识到了什么?

如果在事务事件监听的同步处理中,是个耗时较长的操作,就会一直持有这个数据库连接,线上如果有大量的并发调用,数据库的连接池很容易被耗尽。

想要解决这个问题,可以考虑异步,用新线程去处理这个耗时调用,提前结束回调并释放之前的数据库连接。

总结

在这篇文章中,我们分析了在使用 Spring 的事务监听器时,因为原事务已提交,后续事务加入失败而导致的事务失效问题,解决方案就是将后续事务作为新事物处理。

同时梳理了一下 Spring 事务提交和后续处理的过程,明白了回调操作仍然持有之前的数据库连接,如果耗时过长可能会耗尽连接池,可以通过新线程处理来避免这个问题。

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
19天前
|
缓存 安全 Java
Spring高手之路26——全方位掌握事务监听器
本文深入探讨了Spring事务监听器的设计与实现,包括通过TransactionSynchronization接口和@TransactionalEventListener注解实现事务监听器的方法,并通过实例详细展示了如何在事务生命周期的不同阶段执行自定义逻辑,提供了实际应用场景中的最佳实践。
40 2
Spring高手之路26——全方位掌握事务监听器
|
20天前
|
Java 关系型数据库 数据库
京东面试:聊聊Spring事务?Spring事务的10种失效场景?加入型传播和嵌套型传播有什么区别?
45岁老架构师尼恩分享了Spring事务的核心知识点,包括事务的两种管理方式(编程式和声明式)、@Transactional注解的五大属性(transactionManager、propagation、isolation、timeout、readOnly、rollbackFor)、事务的七种传播行为、事务隔离级别及其与数据库隔离级别的关系,以及Spring事务的10种失效场景。尼恩还强调了面试中如何给出高质量答案,推荐阅读《尼恩Java面试宝典PDF》以提升面试表现。更多技术资料可在公众号【技术自由圈】获取。
|
1月前
|
Java 开发者 Spring
Spring高手之路24——事务类型及传播行为实战指南
本篇文章深入探讨了Spring中的事务管理,特别是事务传播行为(如REQUIRES_NEW和NESTED)的应用与区别。通过详实的示例和优化的时序图,全面解析如何在实际项目中使用这些高级事务控制技巧,以提升开发者的Spring事务管理能力。
52 1
Spring高手之路24——事务类型及传播行为实战指南
|
25天前
|
JavaScript Java 关系型数据库
Spring事务失效的8种场景
本文总结了使用 @Transactional 注解时事务可能失效的几种情况,包括数据库引擎不支持事务、类未被 Spring 管理、方法非 public、自身调用、未配置事务管理器、设置为不支持事务、异常未抛出及异常类型不匹配等。针对这些情况,文章提供了相应的解决建议,帮助开发者排查和解决事务不生效的问题。
|
1月前
|
XML Java 数据库连接
Spring中的事务是如何实现的
Spring中的事务管理机制通过一系列强大的功能和灵活的配置选项,为开发者提供了高效且可靠的事务处理手段。无论是通过注解还是AOP配置,Spring都能轻松实现复杂的事务管理需求。掌握这些工具和最佳实践,能
47 3
|
2月前
|
Java 关系型数据库 MySQL
Spring事务失效,我总结了这7个主要原因
本文详细探讨了Spring事务在日常开发中常见的七个失效原因,包括数据库不支持事务、类不受Spring管理、事务方法非public、异常被捕获、`rollbackFor`属性配置错误、方法内部调用事务方法及事务传播属性使用不当。通过具体示例和源码分析,帮助开发者更好地理解和应用Spring事务机制,避免线上事故。适合所有使用Spring进行业务开发的工程师参考。
39 2
|
2月前
|
设计模式 Java Spring
Spring Boot监听器的底层实现原理
Spring Boot监听器的底层实现原理主要基于观察者模式(也称为发布-订阅模式),这是设计模式中用于实现对象之间一对多依赖的一种常见方式。在Spring Boot中,监听器的实现依赖于Spring框架提供的事件监听机制。
33 1
|
2月前
|
Java 程序员 Spring
Spring事务的1道面试题
每次聊起Spring事务,好像很熟悉,又好像很陌生。本篇通过一道面试题和一些实践,来拆解几个Spring事务的常见坑点。
Spring事务的1道面试题
|
7月前
|
监控 Java 数据库
Spring事务相关配置、案例:转账业务追加日志及事务传播行为
Spring事务相关配置、案例:转账业务追加日志及事务传播行为
83 0
|
存储 Java 数据库
Spring事务和事务传播机制
Spring事务和事务传播机制
101 0
下一篇
DataWorks