支付系统中,像资金下发这种业务,通常是在我们系统发给第三方支付通道后,第三方支付通道会进行资金业务处理。然后,付款完成后,会主动发起回调,即,调用我们系统API,将付款结果通知给我们系统。
假定我们的支付系统对三方通道回调通知的处理逻辑包括:① 修改本地付款单的付款状态;② 将付款结果调用商户系统API,通知给商户。见下方示意图。
用伪代码来表示,大概是下面这样子:
public void payNotify(Order order) {
// ① 修改付款单状态
orderRepository.updatePayResult(order.getId(), "PAY_SUCCESS");
// ② 将付款结果调用商户系统API,通知商户
Merchant merchant = merchantRepository.getById(order.getMerId());
String reqMsg = encrypt("orderNo="+order.getOrderId()+"&state=PAY_SUCCESS");
HttpClient.sendPost(merchant.getNotifyUrl(), reqMsg);
}
为了提高程序性能,开发人使用了异步线程,出现了下面的不靠谱代码:
public void payNotify(Order order) {
// ① 修改付款单状态
new Thread(()->{
orderRepository.updatePayResult(order.getId(), "PAY_SUCCESS");
}).start();
// ② 将付款结果调用商户系统API,通知商户
new Thread(()->{
Merchant merchant = merchantRepository.getById(order.getMerId());
String reqMsg = encrypt("orderNo="+order.getOrderId()+"&state=PAY_SUCCESS");
HttpClient.sendPost(merchant.getNotifyUrl(), reqMsg);
}).start();
}
结果呢,出现了一种情况,当前系统里订单的付款状态没能修改,可是通知商户却成功了。。
我们作为支付服务提供方,我们系统里的付款单不是终态,而商户系统里却是终态。在支付系统中,这样的程序是很要命的。尤其是当我们系统里的付款单是“付款中”而商户系统里是“付款失败”的情况。这时,商户系统可能会因为付款失败而重新发起付款。如果原单实际是付款成功的话,这就出现了资金风险,即,一笔订单付款了两次。就问你怕不怕?
所以,本案中,修改付款单状态的逻辑,必须在主线程执行,确保当前支付系统的付款单状态变更完成后,才可以做后续业务处理。通知商户则可以异步处理,即使通知失败也可以用相关方式来补偿。
这个案例告诉我们,代码中在使用JUC、消息队列、回调函数、消息中间件等提高程序性能的方式进行异步处理时,一定要分清主次,哪些逻辑必须在主线程执行,哪些逻辑可以异步处理。