问题的可能预后
我相信这里的问题与vs无关。这个问题似乎与Spring方法的本质有关,以及在涉及数据库和AMQP代理的分布式环境中错误地使用这些事务,并且可能增加了对JPA上下文如何工作的一些基本误解。save
saveAndFlush
@Transactional
在你的解释中,你似乎暗示你在一个方法中启动你的JPA事务,并且在事务期间(但在提交之前),你向AMQP代理发送消息。稍后,在队列的另一端,使用者应用程序获取消息并进行 REST 服务调用。此时,您注意到来自发布者端的事务更改尚未提交到数据库,因此对使用者端不可见。@Transactional
问题似乎是,在 JPA 事务提交到磁盘之前,您在 JPA 事务中传播这些 AMQP 消息。当使用者读取消息并处理它时,来自发布端的事务可能尚未完成。因此,这些更改对使用者应用程序不可见。
如果您的 AMPQ 实现是 Rabbit,那么我以前见过这个问题。启动使用数据库事务管理器的方法时,在该方法中,使用 a 发送相应的消息。@Transactional
RabbitTemplate
如果您没有使用事务处理通道(即 ),则您的消息在数据库事务提交之前传递。我相信,通过在 中启用事务处理通道(默认情况下禁用),您可以解决部分问题。RabbitTemplate
channelTransacted=true
RabbitTemplate
<rabbit:template id="rabbitTemplate"
connection-factory="connectionFactory"
channel-transacted="true"/>
当通道被事务处理时,则“加入”当前数据库事务(显然是JPA事务)。一旦你的JPA事务提交,它会运行一些尾声代码,这些代码也会提交Rabbit通道中的更改,这会强制实际“发送”消息。RabbitTemplate
关于保存与保存和冲洗
您可能认为刷新 JPA 上下文中的更改应该可以解决问题,但您错了。刷新 JPA 上下文只会强制将实体中的更改(此时仅在内存中)写入磁盘。但是,它们仍会写入相应数据库事务中的磁盘,在 JPA 事务提交之前不会提交。这发生在你的方法结束时(不幸的是,在你已经发送AMQP消息之后的一段时间 - 如果你没有使用如上所述的交易通道)。@Transactional
因此,即使您刷新了 JPA 上下文,在发布者应用程序中完成方法之前,使用者应用程序也不会看到这些更改(根据经典数据库隔离级别规则)。@Transactional
当您调用需求时,不会立即同步任何更改。大多数 JPA 实现只是在内存中将实体标记为脏,并等到最后一分钟才将所有更改与数据库同步,并在数据库级别提交这些更改。save(entity),
EntityManager
注意:在某些情况下,您可能希望其中一些更改立即进入磁盘,直到异想天开的人决定这样做。当数据库表中有一个触发器时,就会发生这种情况的典型示例,您需要运行该触发器来生成稍后在事务期间需要的一些其他记录。因此,强制将更改刷新到磁盘,以便强制运行触发器。EntityManager
通过刷新上下文,您只是强制将内存中的更改同步到磁盘,但这并不意味着这些修改的即时数据库提交。因此,您刷新的这些更改不一定对其他事务可见。最有可能的是,根据传统的数据库隔离级别,他们不会这样做。
2PC 问题
另一个经典问题是,您的数据库和 AMQP 代理是两个独立的系统。如果这是关于Rabbit的,那么你没有2PC(两阶段提交)。
因此,您可能需要考虑一些有趣的场景,例如,您的数据库事务成功提交。尽管如此,Rabbit仍然无法提交您的消息,在这种情况下,您将不得不重复整个事务,可能跳过数据库副作用,只是重新尝试将消息发送给Rabbit。
您可能应该阅读这篇关于Spring分布式事务的文章,无论是否使用XA,特别是关于链事务的部分有助于解决此问题。
他们建议使用更复杂的事务管理器定义。例如:
<bean id="jdbcTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="rabbitTransactionManager" class="org.springframework.amqp.rabbit.transaction.RabbitTransactionManager">
<property name="connectionFactory" ref="connectionFactory"/>
</bean>
<bean id="chainedTransactionManager" class="org.springframework.data.transaction.ChainedTransactionManager">
<constructor-arg name="transactionManagers">
<array>
<ref bean="rabbitTransactionManager"/>
<ref bean="jdbcTransactionManager"/>
</array>
</constructor-arg>
</bean>
然后,在你的代码中,你只需使用链接的事务管理器来协调你的数据库事务部分和 Rabbit 事务部分。
现在,您仍然有可能提交数据库部分,但 Rabbit 事务部分会失败。
所以,想象一下这样的事情:
@Retry
@Transactional("chainedTransactionManager")
public void myServiceOperation() {
if(workNotDone()) {
doDatabaseTransactionWork();
}
sendMessagesToRabbit();
}
通过这种方式,如果您的 Rabbit 事务部分因任何原因而失败,并且您被迫重试整个链接的事务,则可以避免重复数据库副作用,只需确保将失败的消息发送给 Rabbit 即可。
同时,如果您的数据库部分出现故障,那么您从未向Rabbit发送消息,也不会有任何问题。
或者,如果您的数据库副作用是幂等的,那么您可以跳过检查,只需重新应用数据库更改,然后重新尝试将消息发送给Rabbit。
事实是,最初,你试图做的事情似乎非常容易,但是一旦你深入研究不同的问题并理解它们,你就会意识到以正确的方式做到这一点是一项棘手的事情。