Spring @Transactional 挂起函数
Spring @Transactional on suspend function
我现在有点沮丧,因为我认为这会容易得多,问题会得到更好的记录,但我就是找不到解决方案。所以我在这里寻求帮助。
我正在开发一个 Kotlin 项目,该项目利用 spring 引导版本 2.5.3 并使用 spring 数据 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 中使用事务方法暂停工作的开发人员,但我仍然无法找到有关此问题的有用资源。
谢谢大家!
JPA 不支持协程事务,因为 JPA 是完全同步的。协程事务传播仅适用于提供响应式集成的技术,例如 MongoDB、R2DBC 或 Neo4j。
JPA 采用命令式编程模型,因此其事务管理器将事务状态存储在 ThreadLocal
存储中。反应式集成,特别是协程使用协程 context/Reactor 的订阅上下文来跟踪事务状态。 ThreadLocal
和 Coroutines/Project Reactor 的上下文特征之间没有 link。
展望未来,使用阻塞集成(例如 JPA)需要在协程上下文中特别注意,并且需要将它们的事务范围限制在单个线程上。
我现在有点沮丧,因为我认为这会容易得多,问题会得到更好的记录,但我就是找不到解决方案。所以我在这里寻求帮助。
我正在开发一个 Kotlin 项目,该项目利用 spring 引导版本 2.5.3 并使用 spring 数据 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 中使用事务方法暂停工作的开发人员,但我仍然无法找到有关此问题的有用资源。
谢谢大家!
JPA 不支持协程事务,因为 JPA 是完全同步的。协程事务传播仅适用于提供响应式集成的技术,例如 MongoDB、R2DBC 或 Neo4j。
JPA 采用命令式编程模型,因此其事务管理器将事务状态存储在 ThreadLocal
存储中。反应式集成,特别是协程使用协程 context/Reactor 的订阅上下文来跟踪事务状态。 ThreadLocal
和 Coroutines/Project Reactor 的上下文特征之间没有 link。
展望未来,使用阻塞集成(例如 JPA)需要在协程上下文中特别注意,并且需要将它们的事务范围限制在单个线程上。