【问题标题】:How to avoid race conditions for duplicate requests?如何避免重复请求的竞争条件?
【发布时间】:2021-03-12 09:12:19
【问题描述】:

假设我收到两个具有相同负载的并发请求。但我必须 (1) 执行单个支付交易(使用第三方 API)和 (2) 以某种方式对两个请求返回相同的响应。正是第二个要求使事情变得复杂。否则,我可能只会对重复请求返回错误响应。

我有两个实体:SessionPayment(通过 @OneToOne 关系相关)。 Session 有两个字段来跟踪整体状态:PaymentStatusNONEOKERROR)、SessionStatusCHECKED_INCHECKED_OUT)。初始条件为NONECHECKED_IN

请求负载确实包含一个唯一的会话编号,我用它来获取相关会话。现在,假设支付服务对于一个唯一的订单 id 是一种“幂等的”:它只对给定的订单 id 执行一次交易。订单 ID 也包含在请求负载中(双请求的值相同)。

我想到的流程是这样的:

  1. 获取会话
  2. 如果是session.getPaymentStatus() == OK,找到支付并返回成功响应。
  3. 付款
  4. 将付款保存到 DB。 Session 具有从请求有效负载生成的具有唯一约束的字段。因此,如果其中一个线程尝试插入重复项,则会抛出 DataIntegrityViolationException。我抓住它,找到已经插入的付款,并根据它返回响应。
  5. 如果4没有抛出异常,返回相应的响应。

在这个流程中,似乎至少有一种情况我可能必须对两个请求都返回错误响应尽管支付交易已成功完成!例如,假设“第一个”请求发生错误,付款未完成,并返回错误响应。但是对于“第二个”请求,恰好处理时间稍长一些,支付完成了,但是在插入DB时,发现已经插入的支付记录,并在此基础上形成错误响应。

我想避免所有这些类似比赛条件的情况。我有一种感觉,我在这里遗漏了一些非常明显的东西。本质上,问题在于以某种方式发出一个请求以等待另一个请求完成。有没有办法可以利用数据库事务和锁来顺利处理这个问题?

上面我假设支付服务对于给定的订单 ID 是幂等的。如果不是,我必须绝对避免向它发送重复请求怎么办?

这里是服务方法的相关部分:

Session session = sessionRepo.findById(sessionId)
        .orElseThrow(SessionNotFoundException::new);

Payment payment = paymentManager.pay(session, req.getReference(), req.getAmount());

Payment saved;
try {
    saved = paymentRepo.save(payment);
} catch (DataIntegrityViolationException ex) {
    saved = paymentRepo.findByOrderId(req.getReference())
            .orElseThrow(PaymentNotFoundException::new);
}

PaymentStatus status = saved.getSession().getPaymentStatus();
PaymentStage stage = saved.getSession().getPaymentStage();

if (stage == COMPLETION && status == OK)
    return CheckOutResponse.success(req.getTerminalId(), req.getReference(), 
            req.getPlateNumber(), saved.getAmount(), saved.getRrn());

return CheckOutResponse.error(req.getTerminalId(), req.getReference(),
            "Unable to complete transaction.");

【问题讨论】:

    标签: java spring spring-mvc concurrency architecture


    【解决方案1】:

    我认为将 id 分配给实体(希望对于同一请求始终相同)和UNIQUE (id) 约束的组合构成了避免数据库重复的充分条件。

    如果您想(出于我不知道的原因)避免第一个条件,您可以随时检查请求的时间戳或将持久层设计为在更新前“手动”检查重复项。

    但是,与往常一样,问题是您要实现什么目标?这个地方(StackOverflow)更多的是讨论/纠正实现而不是理论问题。

    编辑

    如果我理解正确,这是在某处设置公共静态标志(或标志列表,你明白了)的问题。在您的服务中,您首先检查标志,如果为真,则等待它为假;然后最后执行主要操作。

    至于重复请求,我会将每个请求与最后一个请求进行比较。如果所有参数都相同并且时间戳足够接近,我会返回状态 400 或其他。

    但我仍然不明白您为什么想要相同的回复。当然,您可以在收到每个请求后到实际执行它之前等待任意时间,但为什么不总是允许“唯一”请求继续进行呢?

    【讨论】:

    • 但我的问题不在于避免数据库重复。我已经在这样做了。这是关于让一个正在处理 req1 的线程以某种方式向正在处理 req2 的另一个线程发出信号,等待 req1 线程使用第三方 API 执行付款。
    • 这类似于电子商务应用中可能出现的情况:用户错误地点击了两次支付按钮。我想要实现的是:(1) 进行一次支付交易,(2) 对重复请求返回完全相同的响应。
    • 问题是我在这件事上别无选择:我必须发送相同的回复。真是个坑爹的要求我想我可以使用标志来做到这一点。谢谢。
    【解决方案2】:

    我想避免所有这些类似比赛条件的情况。我有一个 感觉我在这里遗漏了一些非常明显的东西。从本质上讲, 问题是以某种方式提出一个请求以等待另一个请求 完全的。有没有办法可以利用数据库事务和锁 能顺利处理吗?

    我倾向于认为,尽管付款已成功处理,但无法消除返回错误响应的所有可能性,因为有太多地方可能发生损坏,包括您自己的代码之外。但是,是的,您可以通过应用一些锁定来消除一些不一致响应的机会。

    例如,

    1. 获取会话
    2. 获取会话的PaymentStatus对其进行悲观锁定。您还必须包含代码以确保在请求处理完成之前释放此锁,即使在错误情况下也是如此(我对此不再赘述)。
    3. 如果是session.getPaymentStatus() != NONE,则返回对应的响应。
    4. 付款
    5. 将付款保存到数据库,我想这包括将PaymentStatus 更新为OKERROR。由于锁定,预计不会尝试插入副本。如果发生这种情况,则需要通知管理员,并返回不同的响应,可能是 501。
    6. 返回适当的响应。

    请注意,成功付款的幂等性在这方面对您没有帮助,但如果幂等性扩展到付款失败的情况,那么您的原始工作流程将不会受到问题中描述的不一致响应问题的影响.

    【讨论】:

      【解决方案3】:

      您在谈论“相同的有效载荷”。因此,您必须使用实现“相同”概念的 hash/equal 方法创建类 Payload。

      然后,您为所有已启动的有效负载创建一个同步的哈希集。

      处理下一个请求时,如果不存在,则创建新的有效负载并启动它。如果这样的有效载荷已经存在,那么只需返回其结果。即使是现有的payload也不能完成,要舒服地等待它的结果,将payload声明为CompletableFuture。

      【讨论】:

      • 如果我理解正确,这个建议归结为在可能的多个线程中同步两个特定线程。根据您的建议,我为Payload 实现了hashequals 方法,创建了ConcurrentHashMap<Payload, Request>,并在Request (Request#proceed) 内的实例方法上使用了synchronized 关键字。一旦副本到达,两者都从地图中获取Request,但只有第一个可以调用Request#proceed,而另一个则等到第一个完成。反过来,后来者线程只会得到较早者的结果。
      • 我理解正确了吗?我不太明白的一件事是我如何在这个方案中利用CompletableFuture(或者我什至是否需要它)。你能详细说明一下吗?
      • 我在第一条评论中遗漏了一些东西:一旦重复到达,第一个创建 Request 并继续,但第二个获得现有的 Request 并等待。我通过computeIfAbsentPayload 转换为Request
      • 等待Request的结果,CompletableFuture是一个方便的方法。使请求扩展 CompletableFuture。
      猜你喜欢
      • 2019-06-12
      • 1970-01-01
      • 1970-01-01
      • 2019-05-03
      • 1970-01-01
      • 2015-01-30
      • 2010-09-25
      • 2016-09-20
      相关资源
      最近更新 更多