我有一个由一组微服务构建的应用程序。一个服务接收数据,通过Spring JPA和Eclipse链接持久保存,然后向第二个服务发送警报(AMQP)。
根据特定条件,第二个服务然后针对持久数据调用RESTfull Web服务以检索保存的信息。
我注意到有时RESTfull服务返回一个空数据集,即使数据先前已保存过。查看持久服务的代码,已使用 save 而不是 saveandflush ,因此我假设数据的刷新速度不足以让下游服务进行查询。
我应该说原始持久性函数包含在@Transactional
答案 0 :(得分:34)
问题的可能预测
我认为此处的问题与save
vs saveAndFlush
无关。问题似乎与Spring @Transactional
方法的性质有关,并且在涉及数据库和AMQP代理的分布式环境中错误地使用这些事务;也许,加上有毒的混合,对JPA背景如何运作有一些基本的误解。
在您的解释中,您似乎暗示您在@Transactional
方法内开始JPA事务,并且在事务期间(但在提交之前),您将消息发送到AMQP代理;然后,在队列的另一端,使用者应用程序获取消息并进行REST服务调用。此时您注意到发布方的事务更改尚未提交到数据库,因此对于消费者方不可见。
问题似乎是在JPA事务提交到磁盘之前传播这些AMQP消息。当消费者阅读并处理消息时,您发布的交易可能尚未完成。因此,消费者应用程序无法看到这些更改。
如果您的AMPQ实现是Rabbit,那么我之前已经看到过这个问题:当您启动使用数据库事务管理器的@Transactional
方法时,在该方法中使用RabbitTemplate
发送相应的消息。
如果您的RabbitTemplate
未使用事务处理频道(即channelTransacted=true
),则会在提交数据库事务之前传递您的消息。我相信通过启用RabbitTemplate
中的事务处理渠道(默认情况下已禁用),您可以解决部分问题。
<rabbit:template id="rabbitTemplate"
connection-factory="connectionFactory"
channel-transacted="true"/>
当频道成交时,RabbitTemplate
&#34;加入&#34;当前的数据库事务(显然是JPA事务)。一旦您的JPA事务提交,它就会运行一些结尾代码,这些代码也会在您的Rabbit通道中进行更改,这会强制实际发送&#34;发送&#34;消息。
关于save vs saveAndFlush
您可能认为刷新JPA上下文中的更改应该已经解决了问题,但您错了。刷新JPA上下文只会强制将实体中的更改(此时仅在内存中)写入磁盘,但它们仍会在相应的数据库事务中写入磁盘,这将在您的JPA事务提交之前一直提交。这种情况发生在@Transactional
方法的最后(不幸的是,在您发送了AMQP消息后的某个时间 - 如果您不使用上述交易渠道)。
因此,即使您刷新JPA上下文,您的使用者应用程序也不会看到这些更改(根据经典的数据库隔离级别规则),直到您的@Transactional
方法在发布者应用程序中完成。
当您调用save(entity)
时,EntityManager
无需立即同步任何更改。大多数JPA实现只是将实体标记为内存中的脏,并等到最后一刻将所有更改与数据库同步并在数据库级别提交这些更改。
注意:在某些情况下,您可能希望其中一些更改立即转到磁盘,直到异想天开的EntityManager
决定这样做。当数据库表中有一个触发器需要它运行以生成一些您在事务期间稍后需要的其他记录时,会发生这种情况的典型示例。因此,您强制将更改刷新到磁盘,以便强制触发器运行。
通过刷新上下文,您只是强制将内存中的更改同步到磁盘,但这并不意味着即时数据库提交这些修改。因此,您刷新的那些更改不一定对其他交易可见。基于传统的数据库隔离级别,他们很可能不会获胜。
2PC问题
这里的另一个经典问题是您的数据库和AMQP代理是两个独立的系统。如果这是关于Rabbit的,那么你就没有2PC(两阶段提交)。
所以你可能想要考虑有趣的场景,例如:您的数据库事务成功提交,但是Rabbit无法提交您的消息,在这种情况下您将不得不重复整个事务,可能会跳过数据库副作用,只是重新尝试将消息发送到Rabbit。
您应该阅读Distributed transactions in Spring, with and without 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,并且没有问题。
或者,如果您的数据库副作用是幂等的,那么您可以跳过检查,只需重新应用数据库更改,然后重新尝试将消息发送给兔子。
事实是,最初你想要做的事情似乎很容易,但是一旦你深入研究了不同的问题并理解它们,你就会意识到以正确的方式做这件事是一项棘手的事情。