【发布时间】:2021-03-12 09:12:19
【问题描述】:
假设我收到两个具有相同负载的并发请求。但我必须 (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,找到支付并返回成功响应。 - 付款
- 将付款保存到 DB。
Session具有从请求有效负载生成的具有唯一约束的字段。因此,如果其中一个线程尝试插入重复项,则会抛出DataIntegrityViolationException。我抓住它,找到已经插入的付款,并根据它返回响应。 - 如果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