Spring JPA:节省和节省的成本是多少?

2022-09-01 18:06:24

我有一个从一组微服务构建的应用程序。一个服务接收数据,通过Spring JPA和Eclipse链接将其持久化,然后向第二个服务发送警报(AMQP)。

然后,第二个服务根据特定条件对持久化数据调用 RESTfull Web 服务以检索保存的信息。

我注意到,有时 RESTfull 服务会返回一个空数据集,即使之前已保存数据也是如此。查看持久化服务的代码,已使用 save 而不是 saveandflush,因此我假设数据刷新的速度不够快,无法查询下游服务。

  • 是否有我应该厌倦的节省和冲洗成本,或者默认情况下使用它是否合理?
  • 它能否确保下游应用程序的数据即时可用性?

我应该说,原始的持久性函数被包装在@Transactional


答案 1

问题的可能预后

我相信这里的问题与vs无关。这个问题似乎与Spring方法的本质有关,以及在涉及数据库和AMQP代理的分布式环境中错误地使用这些事务,并且可能增加了对JPA上下文如何工作的一些基本误解。savesaveAndFlush@Transactional

在你的解释中,你似乎暗示你在一个方法中启动你的JPA事务,并且在事务期间(但在提交之前),你向AMQP代理发送消息。稍后,在队列的另一端,使用者应用程序获取消息并进行 REST 服务调用。此时,您注意到来自发布者端的事务更改尚未提交到数据库,因此对使用者端不可见。@Transactional

问题似乎是,在 JPA 事务提交到磁盘之前,您在 JPA 事务中传播这些 AMQP 消息。当使用者读取消息并处理它时,来自发布端的事务可能尚未完成。因此,这些更改对使用者应用程序不可见。

如果您的 AMPQ 实现是 Rabbit,那么我以前见过这个问题。启动使用数据库事务管理器的方法时,在该方法中,使用 a 发送相应的消息。@TransactionalRabbitTemplate

如果您没有使用事务处理通道(即 ),则您的消息在数据库事务提交之前传递。我相信,通过在 中启用事务处理通道(默认情况下禁用),您可以解决部分问题。RabbitTemplatechannelTransacted=trueRabbitTemplate

<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。

事实是,最初,你试图做的事情似乎非常容易,但是一旦你深入研究不同的问题并理解它们,你就会意识到以正确的方式做到这一点是一项棘手的事情。


答案 2

推荐