失去的更新 - Java,Spring和JPA

时间:2017-09-12 15:22:29

标签: java spring hibernate jpa concurrency

我在工作中遇到了一个问题,几个月来一直试图解决它,这让我疯狂。

事情很难解释,它涉及到我不允许讨论的域名的某些特殊性,我无法复制粘贴确切的代码。我将尝试通过一些有代表性的例子尽可能地清楚自己。

简单来说,系统包含一个根实体,我们称之为MainDocument实体。围绕这个实体,有几个实体在轨道运行。 MainDocument实体有一个州。我们将此状态称为“MainDocumentState”。

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

有大约10个州可用,但在这个例子中将重点关注其中两个。让我们打电话给他们,ReadyForAuthorizationAuthorized

这就是你需要知道的所有例子。

关于我们正在使用的技术:

  1. 春天
  2. GWT Webapp
  3. Java 1.6
  4. 休眠
  5. JPA
  6. Oracle DB。
  7. 关于问题本身:

    系统中有一部分是关键的,并处理大部分传入流量。我们将此部分称为“授权部分”。在本节中,我们通过我们国家的海关和边境保护局提供的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);
       }
      }
    

    什么时候丢失更新以及它如何影响系统:

    1. MainDocument id:1 @ Thread-1尝试授权
    2. 文档未经授权,继续执行
    3. 浏览网络服务并授权确定
    4. 交易关闭并提交。
    5. 当1正在提交时,MainDocument 1 @ Thread-2进来,并尝试 认证。
    6. 1尚未保留,Thread-2尝试授权。
    7. WS拒绝了线程-2,并且响应“文档1已经被授权”。
    8. Thread-2尝试提交。
    9. Thread-1首先提交文档1,Thread-2提交到第二位。
    10. ID为1的MainDocument以状态ReadyForAuthorization保留,而正确的状态应为Authorized。

      复杂性的产生是因为它几乎不可能再现。它只发生在生产中,即使我试图用数百个电话充斥服务器,我也无法得到相同的行为。

      实施解决方案:

      1. 线程屏障,如果两个具有相同MainDocument标识的线程尝试授权,则拒绝输入的最后一个。它是通过一个方面实现的,订单为100,因此它在@Transactional提交之后执行。在方面拦截之前对事务提交的堆栈跟踪进行测试和检查,并从屏障中删除线程。
      2. @Version,适用于系统的其他部分,当一个提交尝试覆盖旧事务的另一个提交时,引发OptimisticLockException。在这种情况下,不会引发OptimisticLockException。
      3. “Transaction”与@Transactional(传播= REQUIRES_NEW)保持一致,因此它独立于主事务并且正确提交。通过此事务,很明显丢失更新是一个问题,因为我们可以看到已完成的事务与成功消息,并且MainDocument保持不同状态,server.log上没有显示错误。
      4. 使用 Imperva SecureSphere ,我们可以审核特定表格上的所有更新。我们可以清楚地看到第一个事务以正确的状态提交,第二个事务覆盖第一个事务。
      5. 如果有并发和交易管理经验的人可以给我一些有关如何调试或重现问题的有用提示,或者至少实施一些解决方案以减轻损失,我将不胜感激。

        需要说明的是,每小时有超过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;
          }
        

1 个答案:

答案 0 :(得分:1)

主要问题是并发性。 您的代码现在的样子,它试图检查实体是否已获得授权,何时应检查它是否已获得授权或正在获得授权。

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

我遇到过一些看似相似的情况,包括代码在集群中运行的情况。我找到的最佳工作解决方案是使用某种形式o数据库锁。

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

没有@Version,我会尝试一些&#34; hardcore&#34;悲观的数据库锁。建议的解决方案当然不是唯一的,也不是最好的解决方案。

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。