【问题标题】:Does a hibernate transaction commit changes asynchronously, independent of the program flow?休眠事务提交是否异步更改,独立于程序流?
【发布时间】:2021-02-05 19:05:58
【问题描述】:

我有一个测试方法,在部署过程中有时会失败,有时不会。我从未见过它在我的本地失败。你可以在下面看到我的代码。

我有以下从另一个服务异步调用的重试机制:

@Transactional
public boolean retry(NotificationOrder order) {
    notificationService.send(order);
    return true;
}

public void resolveOnFailedAttempt(Long orderId) {  //automatically called if `retry` method fails
    notificationOrderCommonTransactionsService.updateNotificationOrderRetryCount(orderId);
}

通知服务是这样的:

@Service
@RequiredArgsConstructor
public class NotificationServiceImpl implements NotificationService {

    private final NotificationOrderCommonTransactionsService notificationOrderCommonTransactionsService;

    @Override
    @Transactional
    public NotificationResponse send(NotificationOrder order) {
        NotificationRequest request;
        try {
            request = prepareNotificationRequest(order);
        } catch (Exception e) {
            notificationOrderCommonTransactionsService.saveNotificationOrderErrorMessage(order.getId(),
                          e.getMessage());
            throw e;
        }

        ...
 
        return response;
    }

        private void prepareNotificationRequest(NotificationOrder order) {
            ...
            throw new Exception("ERROR");
        }
}


而常见的交易服务是这样的:

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public NotificationOrder saveNotificationOrderErrorMessage(Long orderId, String errorMessage) {
        NotificationOrder order = notificationRepository.findOne(orderId);
        order.setErrorDescription(errorMessage);
        notificationRepository.save(order);
        return order;
    }

    public NotificationOrder updateNotificationOrderRetryCount(Long orderId) {
        NotificationOrder order = notificationRepository.findOne(orderId);
        order.setRetryCount(order.getRetryCount() + 1);
        order.setOrderStatus(NotificationOrderStatus.IN_PROGRESS);
        notificationRepository.save(order);
        return order;
    }

这是我的集成测试:

    @Test
    public void test() {
 
        NotificationOrderRequest invalidRequest = invalidRequest();

        ResponseEntity<NotificationOrderResponse> responseEntity = send(invalidRequest);

        NotificationOrder notificationOrder = notificationOrderRepository.findOne(1);

        softly.assertThat(notificationOrder.getOrderStatus().isEqualTo(NotificationOrderStatus.IN_PROGRESS))
        softly.assertThat(notificationOrder.getErrorDescription())
                 .isEqualTo("ERROR");  //This the line that fails.

        softly.assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
    }

在测试方法中确认调用updateNotificationOrderRetryCount,订单状态更新为IN_PROGRESS。但是,错误消息为空,我收到以下断言错误:

    -- failure 1 --
    Expecting:
     <null>
    to be equal to:
     <"ERROR">
    but was not.

我希望saveNotificationOrderErrorMessage 事务完成并且在调用updateNotificationOrderRetryCount 方法之前提交更改。但它似乎确实是这样工作的。谁能帮我找出我的代码为什么会这样?

如何在本地重现此错误?我能做些什么来解决它?

谢谢。

【问题讨论】:

  • 您实际上在prepareNotificationRequestan send 方法中抛出了什么样的异常。是RuntimeException吗?它没有在方法签名中声明。
  • 我抛出了一个扩展 RuntimeException 的类的异常。
  • 是否可以假设通知订单的 id 始终为 1?是否有其他测试使用相同的存储库?您的测试数据存储的生命周期是什么?

标签: java spring hibernate transactions assertj


【解决方案1】:

尝试启用 SQL 日志记录和参数绑定日志记录并查看语句。我不知道你所有的代码,但也许你在某处将消息设置为空?也可能是,这些动作以某种方式交错,使得updateNotificationOrderRetryCountsaveNotificationOrderErrorMessage 之前/同时被调用,导致这种情况发生。如果两者都在提交之前运行,但 saveNotificationOrderErrorMessageupdateNotificationOrderRetryCount 之前提交,您可能会看到错误消息被 null 覆盖。

【讨论】:

  • 我没有在我的代码中将消息设置为 null。您说“如果两者都在提交之前运行,但 saveNotificationOrderErrorMessage 在 updateNotificationOrderRetryCount 之前提交,您可能会看到错误消息被 null 覆盖。”我怎样才能在我的本地重现这个?我能做些什么来防止它?任何帮助表示赞赏。谢谢。
  • 您可以设置一个仅停止当前线程而不是整个 VM 的断点,并让其他线程继续执行以强制执行此排序。如果这是可能的,您应该能够看到这种情况发生。
【解决方案2】:

如果问题的代码sn-p是准确的,请注意你是在重新抛出prepareNotificationRequest方法中引发的异常,我假设是为了启用重试机制:

NotificationRequest request;
try {
    request = prepareNotificationRequest(order);
} catch (Exception e) {
    notificationOrderCommonTransactionsService.saveNotificationOrderErrorMessage(order.getId(),
                  e.getMessage());
    throw e; // You are rethrowing the exception
}

对于您的评论,抛出的异常扩展RuntimeException

正如Spring documentation 所示:

在其默认配置中,Spring 框架的事务基础结构代码仅在运行时、未经检查的异常情况下将事务标记为回滚。也就是说,当抛出的异常是 RuntimeException 的实例或子类时。 (默认情况下,错误实例也会导致回滚)。从事务方法抛出的检查异常不会导致默认配置回滚。

可能 Spring 正在执行与 saveNotificationOrderErrorMessage 关联的初始事务的回滚。我意识到这个方法被注释为@Transactional(propagation = Propagation.REQUIRES_NEW) 并且它被注释为is initiating a new transaction,但也许问题可能与它有关。

当重试机制发生时,另一个与updateNotificationOrderRetryCount方法调用相关的事务被执行,并且这个事务被成功提交。这就是正确提交在第二种方法中执行的更改的原因。

问题的解决方案将取决于您的重试机制是如何实现的,但是您可以,例如,引发原始异常,并作为重试机制的第一步,在数据库中跟踪问题,或者引发一个已检查异常 - 默认情况下 Spring 不会对其执行回滚 - 并酌情处理它。

更新

问题的另一个可能原因可能是send 方法中的事务分界。

这个方法被注释为@Transactional。因此,Spring 将为它启动一个新事务。

发生错误,您在数据库中跟踪错误,在新事务中,但请注意初始事务仍然存在。

虽然您的代码中没有描述,但在某种程度上,重试机制发生了,并更新了重试计数。如果此操作在初始事务(或更高级别的事务)中执行,由于事务边界、数据库隔离级别和相关内容,此事务(初始)可能会从事务边界观点,NotificationOrder。而这个信息是最终提交的,覆盖了错误的信息。我希望你能明白。

一个简单的解决方案(可能同时适用于这两种可能性)可能是将错误消息包含在 updateNotificationOrderRetryCount 方法本身中,从而将问题简化为单个事务:

/* If appropriate, mark it as Transactional */
@Transactional
public NotificationOrder updateNotificationOrderRetryCount(Long orderId, String errorMessage) {
  NotificationOrder order = notificationRepository.findOne(orderId);
  order.setRetryCount(order.getRetryCount() + 1);
  order.setOrderStatus(NotificationOrderStatus.IN_PROGRESS);
  order.setErrorDescription(errorMessage);
  // It is unnecessary, all the changes performed in the entity within the transaction will be committed
  // notificationRepository.save(order); 
  return order;
}

【讨论】:

  • 感谢您的回答。 saveNotificationOrderErrorMessage 上有一个注解 @Transactional(propagation = Propagation.REQUIRES_NEW)。这可以防止回滚,因为操作是在新事务中执行的,不受异常影响。顺便说一句,这种测试失败并不总是发生。它有时有效,有时无效。
  • 感谢您的评论@byksl。请注意,send 方法本身被注释为@Transactional,因此它可能以某种方式定义了整个事务行为,尽管方法saveNotificationOrderErrorMessage 被注释为REQUIRES_NEW。我总是发现在单个方法调用中混合多个事务分界会令人困惑。如果可能,请尝试提出另一种异常并查看它是否有效,我认为它可能会起作用,至少如果它类似于您的测试。
猜你喜欢
  • 2018-05-06
  • 2019-09-22
  • 2016-06-16
  • 2021-12-08
  • 1970-01-01
  • 2011-04-05
  • 2013-05-04
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多