假设我收到两个具有相同有效负载的并发请求。但是我必须(1)执行一次付款交易(使用第三方API),并且(2)以某种方式对两个请求返回相同的响应。正是第二个要求使事情变得复杂。否则,我可能刚刚对重复的请求返回了错误响应。
我有两个实体:Session
和Payment
(通过@OneToOne
关系关联)。Session
有两个字段,以保持整体状态的轨迹:PaymentStatus
(NONE
,OK
,ERROR
), (SessionStatus
,)。CHECKED_IN
CHECKED_OUT
初始条件为NONE
和CHECKED_IN
。
请求有效负载确实包含一个唯一的会话号,我用它来获取相关的会话。现在,假设付款服务对于唯一的订单ID来说是“幂等的”:对于给定的订单ID,它仅执行一次交易。订单ID也会出现在请求有效负载中(两个请求的值相同)。
我想到的流程大致如下:
session.getPaymentStatus() == OK
,找到付款并返回成功响应。Session
有一个具有从请求有效负载生成的唯一约束的字段。因此,如果其中一个线程尝试插入副本,DataIntegrityViolationException
则会抛出a。我抓住了它,找到了已经插入的付款,并根据它返回了响应。在此流程中,似乎至少有一种情况,尽管付款交易已成功完成,但我可能不得不对两个请求都返回错误响应!例如,假设“第一个”请求发生了错误,付款未完成,并且返回了错误响应。但是对于“第二个”请求(恰好需要更长的处理时间),完成了付款,但是在插入到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.");
你所说的是“相同的负载”。因此,你必须使用实现“相同”概念的哈希/等值方法来创建有效载荷类。
然后,为曾经启动的所有有效负载创建一个同步的哈希集。
处理下一个请求时,如果不存在,则创建新的有效负载,然后启动它。如果这样的有效载荷已经存在,则只需返回其结果。甚至现有的有效负载也无法完成,以轻松地等待其结果将有效负载声明为CompletableFuture。
如果我对您的理解正确,那么这个建议可以归结为在潜在的多个线程中同步两个特定的线程。根据您的建议,我在()中的实例方法上实现的
hash
和equals
方法,并Payload
创建了ConcurrentHashMap<Payload, Request>
和使用synchronized
关键字。副本到达后,两者都会从地图上获取,但是只有第一个副本可以调用,而另一个副本则等待直到第一个副本结束。反过来,后发线程将只得到较早线程的结果。Request
Request#proceed
Request
Request#proceed
我对你的理解正确吗?我不太了解的一件事是如何利用
CompletableFuture
这种方案(或者是否需要)。您能详细说明一下吗?我在第一条评论中遗漏了一些内容:一旦重复项到达,第一个重复项将创建
Request
和继续,但是第二个重复项将获取现有项Request
并等待。我转换Payload
为Request
viacomputeIfAbsent
。为了等待Request的结果,CompletableFuture是一种方便的方法。使请求扩展CompletableFuture。