【问题标题】:Spring @Transactional on suspend functionSpring @Transactional 挂起功能
【发布时间】:2021-10-05 22:54:34
【问题描述】:

我现在有点沮丧,因为我认为这会容易得多,而且问题会更好地记录在案,但我就是找不到解决方案。因此,我在这里寻求帮助。

我正在开发一个 Kotlin 项目,该项目利用 Spring Boot 2.5.3 版并使用 Spring Data JPA 进行数据库访问和模式定义。很常见也很直接。现在假设我们有某种UserService,其中包含一个方法updateUsername,它获取username作为参数,并在外部服务验证其有效性后更新用户名。为了演示我想强调的问题,在我们验证用户名之前,我们手动将用户名设置为"foo"。这整个工作单元应该发生在事务中,这就是为什么该方法使用@Transactional 注释的原因。但是由于调用了外部服务,当我们等待 http 响应时,该方法将暂停(注意两个方法上的 suspend 关键字)。

@Service
class UserService(private val userRepository: UserRepository) {
    @Transactional
    suspend fun setUsername(id: UUID, username: String): Person {
        logger.info { "Updating username..." }
        val user = userRepository.findByIdentityId(identityId = id)
            ?: throw IllegalArgumentException("User does not exist!")

        // we update the username here but the change will be overridden after the verification to the actual username!
        user.userName = "foo"

        verifyUsername(username)

        user.userName = username
        return userRepository.save(user)
    }
    
    private suspend fun verifyUsername(username: String) {
        // assume we are doing some kind of network call here which will suspend while waiting got a response
        logger.info { "Verifying..." }
        delay(1000)
        logger.info { "Finished verifying!" }
    }
}

这编译成功,我也可以执行该方法并启动一个新事务,但是一旦我们在调用delay(1000) 时暂停verifyUsername 方法的调用,该事务将被提交。因此,我们的数据库将实际保存值“foo”作为用户名,直到它被覆盖。但是如果verifyUsername 之后的代码会失败并抛出异常,我们就无法回滚这个更改,因为事务已经提交并且 foo 将永远留在数据库中!!!这绝对不是预期的行为,因为我们只想在方法的最后提交事务,因此如果出现问题,我们可以随时回滚事务。在这里你可以看到日志:

DEBUG o.s.orm.jpa.JpaTransactionManager - Creating new transaction with name [x.x.x.UserService.setUsername]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
DEBUG o.s.orm.jpa.JpaTransactionManager - Opened new EntityManager [SessionImpl(1406394125<open>)] for JPA transaction
DEBUG o.s.orm.jpa.JpaTransactionManager - Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@9ec4d42]
INFO  x.x.x.UserService - Updating username...
DEBUG org.hibernate.SQL - select person0_.id as id1_6_, person0_.email as email2_6_, person0_.family_name as family_n3_6_, person0_.given_name as given_na4_6_, person0_.identity_id as identity5_6_, person0_.user_name as user_nam6_6_ from person person0_ where person0_.identity_id=?
INFO  x.x.x.UserService - Verifying...
DEBUG o.s.orm.jpa.JpaTransactionManager - Initiating transaction commit
DEBUG o.s.orm.jpa.JpaTransactionManager - Committing JPA transaction on EntityManager [SessionImpl(1406394125<open>)]
DEBUG org.hibernate.SQL - update person set email=?, family_name=?, given_name=?, identity_id=?, user_name=? where id=?
DEBUG o.s.orm.jpa.JpaTransactionManager - Closing JPA EntityManager [SessionImpl(1406394125<open>)] after transaction
INFO  x.x.x.UserService - Finished verifying
DEBUG o.s.orm.jpa.JpaTransactionManager - Creating new transaction with name [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
DEBUG o.s.orm.jpa.JpaTransactionManager - Opened new EntityManager [SessionImpl(319912425<open>)] for JPA transaction
DEBUG o.s.orm.jpa.JpaTransactionManager - Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@1dd261da]
DEBUG org.hibernate.SQL - select person0_.id as id1_6_0_, person0_.email as email2_6_0_, person0_.family_name as family_n3_6_0_, person0_.given_name as given_na4_6_0_, person0_.identity_id as identity5_6_0_, person0_.user_name as user_nam6_6_0_ from person person0_ where person0_.id=?
DEBUG o.s.orm.jpa.JpaTransactionManager - Initiating transaction commit
DEBUG o.s.orm.jpa.JpaTransactionManager - Committing JPA transaction on EntityManager [SessionImpl(319912425<open>)]
DEBUG org.hibernate.SQL - update person set email=?, family_name=?, given_name=?, identity_id=?, user_name=? where id=?
DEBUG o.s.orm.jpa.JpaTransactionManager - Closing JPA EntityManager [SessionImpl(319912425<open>)] after transaction

在这个spring article 中,它说“协程上的事务通过 Spring Framework 5.2 提供的反应式事务管理的编程变体得到支持。对于暂停功能,提供了一个 TransactionalOperator.executeAndAwait 扩展。”

这是否意味着 @Transactional 不能用于挂起方法,您应该以编程方式处理事务管理?更新:This thread 声明 @Transactional 应该适用于挂起功能。

我也知道,我所有的数据库操作都是同步运行的,我可以使用r2dbc 使它们异步运行(只要我的数据库提供实现规范的驱动程序),但我认为我的问题不与我如何与数据库通信有关,但更多的是关于 spring 如何使用 @Transactional 注释方法处理挂起调用。

您对此有何想法和建议?我绝对不是第一个在 Kotlin 中以事务方法暂停工作的开发人员,我仍然无法找到有关此问题的有用资源。

谢谢大家!

【问题讨论】:

  • 我一直在尝试自定义TransactionInterceptor 以支持协程,我采用的方法是使用ThreadContextElement 恢复TransacionSyncronizationManager 状态。 Spring 的代码库很大,因此我将探索限制在 JPA 上下文中,以供参考,如果有人想更深入地改进代码,可以找到 here 它尚未准备好生产,但可以用于探索。

标签: spring spring-boot kotlin spring-data-jpa kotlin-coroutines


【解决方案1】:

协程事务不支持 JPA,因为 JPA 是完全同步的。协程事务传播仅适用于提供响应式集成的技术,例如 MongoDB、R2DBC 或 Neo4j。

JPA 采用命令式编程模型,因此其事务管理器将事务状态存储在 ThreadLocal 存储中。反应式集成,特别是 Coroutines 使用 Coroutines 上下文/Reactor 的订阅上下文来跟踪事务状态。 ThreadLocal 和 Coroutines/Project Reactor 的上下文特性之间没有联系。

展望未来,使用 JPA 等阻塞集成需要在协程上下文中特别注意,并且它们的事务范围需要限制在单个线程上。

【讨论】:

  • 感谢您的回复! :) 我知道 JPA 是完全同步的,事务状态绑定到 ThreadLocal 存储。但是this github issue 声明,@Transactional 现在支持挂起功能。从技术上讲,我猜他们会检查 @Transactional 注释是否在挂起方法上,如果是,他们可能会将事务状态绑定到协程上下文而不是 ThreadLocal 存储。
  • 还有更多内容。协程支持建立在反应式基础设施之上。 Spring 的AbstractPlatformTransactionManagerJpaTransactionManager 不知道发生了什么反应。此外,JPA 的 EntityManager 仅在托管事务的线程上有效,并且由于该概念不适用于协程或反应式编程,因此我们无能为力。
  • 好吧,我了解这些限制。所以关于我上面提到的用例,我想我有两个选择,对吧? 1. 标记为@Transactional 的方法不应被挂起。因此,我应该在runBlocking 中调用verifyUsername,不幸的是这会阻塞我们当前正在处理的线程,直到verifyUsername 完成。 2. 使用 R2DBC 数据库驱动程序(例如io.r2dbc:r2dbc-postgresql)和spring-boot-starter-data-r2dbc 来利用反应式数据库事务。所以我可以再次将我的挂起方法标记为@Transactional。你同意吗?
  • 你怎么看@mp911de?​​span>
猜你喜欢
  • 1970-01-01
  • 2011-12-31
  • 2020-03-13
  • 1970-01-01
  • 1970-01-01
  • 2012-04-18
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多