gpt4 book ai didi

java - 丢失的更新-Java,Spring和JPA

转载 作者:塔克拉玛干 更新时间:2023-11-02 19:49:29 26 4
gpt4 key购买 nike

我在工作中遇到一个问题,尝试了几个月才解决,这让我发疯。

这件事很难解释,它涉及我不允许讨论的领域的某些特殊性,而且我无法复制粘贴确切的代码。我将通过一些代表性的例子来使自己尽可能清楚。

简而言之,系统包含一个根实体,我们称其为MainDocument实体。在这个实体周围,有几个绕行的实体。 MainDocument实体具有状态。我们将此状态称为“MainDocumentState”。

public class MainDocument {
@OneToOne
@JoinColumn(name = "document_state_id")
MainDocumentState state;
@Version
long version = 0L;
}

大约有10个州可用,但是在此示例中,将重点介绍其中两个州。我们称它们为 ReadyForAuthorizationAuthorized

这就是您需要知道的所有示例。

关于我们正在使用的技术:
  • 春天
  • GWT Web应用程序
  • Java 1.6
  • 休眠
  • JPA
  • Oracle数据库。

  • 关于问题本身:

    系统的一部分至关重要,它可以处理大多数传入流量。我们将此部分称为“授权部分”。在本节中,我们通过由美国海关和边境保护局提供的SOAP WS发送信息,以授权针对海关的 MainDocument

    代码如下:
    @Transactional
    public void authorize(Integer mainDocId) {

    MainDocument mainDocument = mainDocumentService.findById(mainDocId);
    // if document is not found, an exception is thrown.
    Assert.isTrue(mainDocument.notAutorized(), "The document is already authorized");
    // more bussiness logic validations happen here. This validations are not important for the topic discussed here. They make sure that the document meets some basic preconditions.

    try {

    Transaction aTransaction = transactionService.newTransaction(); // creates a transaction, an entity stored in the database that keeps track of all the authorization service calls
    try {
    Response response = wsAuthroizationService.sendAuthorization(mainDocument.getId(), mainDocument.getAuthorizationId()); // take into account that sometimes this call can take between 2-4 minutes.
    catch (Exception e) {
    aTransaction.failed();
    transactionService.saveOrUpdate(aTransaction);
    throw e;
    }
    // the behaviour is the same for every error code.
    if (response.getCode() != 0) {
    aTransaction.setErrorCode(resposne.getCode());
    transactionService.saveOrUpdate(aTransaction);
    throw AuthroizationError("Error on auth");
    }
    aTransaction.completed();
    mainDocument.setAuthorizationCode(0);
    mainDocument.authorize(); // will change state to "Authorized"
    } catch (Exception e) {
    mainDocument.authorize(); // will not change state because authorizationCode != 0 or its null.
    } finally {
    saveOrUpdate(mainDocument);
    }
    }

    丢失的更新何时发生以及如何影响系统:
  • MainDocument ID:1 @ Thread-1尝试授权
  • 该文档未经授权,继续执行
  • 浏览网络服务并授权OK
  • 事务关闭并发生提交。
  • 在提交1时,MainDocument 1 @ Thread-2进入并尝试
    进行身份验证。
  • 1尚未持久,线程2尝试进行身份验证。
  • WS拒绝
  • Thread-2,并返回响应“文档1已被授权”。
  • 线程2尝试提交。
  • Thread-1首先提交文档1,Thread-2在第二位提交。

  • ID为1的MainDocument保持状态为ReadyForAuthorization,而正确的状态应为Authorized。

    之所以会产生复杂性,是因为几乎无法复制。它仅在生产中发生,即使我尝试向服务器充斥数百个调用,也无法获得相同的行为。

    实施的解决方案:
  • 线程屏障,如果两个具有相同MainDocument id的线程尝试进行授权,则最后输入的线程将被拒绝。它是用方面实现的,顺序为100,因此在@Transactional提交之后执行。在方面拦截并从屏障中删除线程之前,对事务提交的堆栈跟踪进行测试和检查。
  • @Version,可在系统的其他部分使用,当一个提交尝试覆盖较旧事务中的另一个提交时,将引发OptimisticLockException。在这种情况下,不会引发OptimisticLockException。
  • “Transaction”与@Transactional(propagation = REQUIRES_NEW)保持一致,因此它与主事务无关,并且已正确提交。通过此事务,很明显丢失更新是一个问题,因为我们可以看到带有成功消息的已完成事务,并且MainDocument保持不同状态,而server.log上没有错误显示。
  • 使用 Imperva SecureSphere ,我们可以审核特定表上的所有更新。我们可以清楚地看到第一个事务以正确的状态提交,第二个事务覆盖了第一个事务。

  • 如果有人具有并发和事务管理经验,可以给我一些有关如何调试或重现此问题的技巧,或者至少实施一些减轻损失的解决方案,我将不胜感激。

    需要说明的是,每小时有1000多个请求,其中99.99%正确结束。每月出现此问题的案例总数约为20。

    添加09-13-17:

    如果需要,我们使用的 saveOrUpdate方法:
       * "http://blog.xebia.com/2009/03/23/jpa-implementation-patterns-saving-detached-entities/" >JPA
    * implementation patterns: Saving (detached) entities</a>
    *
    * @param entity
    */
    protected E saveOrUpdate(E entity) {
    if (entity.getId() == null) {
    getJpaTemplate().persist(entity);
    return entity;
    }
    if (!getJpaTemplate().getEntityManager().contains(entity)) {
    return merge(entity);
    }
    return entity;
    }

    最佳答案

    主要问题是并发性。
    您的代码现在看起来是这样的,它试图检查实体是否被授权,何时应该检查实体是否被授权或者正在被授权。

    它导致了一个重要的问题:
    如何检查实体是否已在整个系统中被操纵?

    我遇到过一些看起来很相似的情况,包括在集群中运行代码的场景。我发现最好的工作解决方案是使用某种形式的数据库锁。

    @Version应该是一个很好的快速解决方案,但是您表示它无法正常工作。您还说过,您可以使用工具审核数据库,在这种情况下,检查版本字段的表现会很有趣。

    如果没有@Version,我将尝试一些“核心”悲观数据库锁定。提出的解决方案当然不是唯一的,也不是最佳的解决方案。

    1-创建一个新表。该表将存储正在处理的文档的ID。 PK应该是文档ID,或其他可以确保同一文档在此表中没有重复的内容。

    2-在检索实体之前,在您的代码中检查ID是否在步骤1中创建的表中。如果不是,请继续。
    如果是这样,则假定它正在处理中并且什么也不做。

    3-在检索实体
    之后,在代码中,您必须在步骤1中创建的表中插入ID。
    如果未授权文档,则插入将成功,并且过程将继续。
    如果有机会同时执行两个请求,则其中一个请求将获得约束违例异常(或类似的异常)。然后,您的代码应假定该文档已被授权。
    重要:必须在新事务中执行插入。用于将Id保留在新表中的spring bean应该将其方法标记为@Transaction(propagation = Propagation.REQUIRES_NEW)

    4-调用Webservice并正确处理了响应后,从步骤1中创建的表中删除ID。它也应在单独的事务中执行。
    考虑在finally块中执行此操作,因为如果发生任何其他运行时错误,则应从表中删除文档ID。

    如何调试:

  • 在本地环境中运行该应用程序,并在之后放置一个断点
    检索实体,并在将其插入新表之前。如果要调试当前代码,则将断点放在Assert语句之后。
  • 在您的开发机中打开两个不同的浏览器,并执行触发此代码的用例。您也可以要求团队成员在其计算机上执行该操作。
  • 您应该看到您的IDE两次在断点处显示正在执行的代码。之后,让两个执行程序一次又一次地运行,并欣赏表演。该场景应被复制。
  • 基本上,这模拟了两个同时请求。

  • 注意事项:
  • 我选择使用数据库表,因为即使该应用程序已部署在集群环境(多个应用程序服务器实例)中,该解决方案仍然可以使用。
  • 如果仅运行一个实例,则可以尝试使用跨请求共享的对象,但是如果将来需要使用群集扩展应用程序,则该解决方案将无法使用。另外,您还必须处理线程安全性。
  • 您也可以尝试使用数据库锁定,但是必须注意不要将表/行锁定太长时间。而且,JPA没有任何特定的操作来对表/行执行锁定(至少我找不到一个),因此您必须处理本机SQL。
  • 关于java - 丢失的更新-Java,Spring和JPA,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/46180196/

    26 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com