假如,有一笔订单,在10:00下单成功,超时时间是1小时,那么在11点的时候,支付成功了,这时候该如何处理?
这是一个比较常见的一个并发处理的问题,而且也是业务中比较常见的问题,一个简单的支付单的状态机如下:
在"支付中"的一笔支付单 ,是有可能推进到支付成功的状态,并且也可能推进到已取消的状态的。
一般来说,正常的支付业务中,支付成功和已取消,都应该是终态,也就是状态机中的最终状态,终态是不能再变化的。 如果一个模型没有明确的终态,或者已经终态的终态数据状态还能随便变化,那么他的设计一定是不合理的。
那么,如果刚好在同一时刻,同一笔支付单同时来了一个支付成功的消息,和一个超时关闭的请求,该如何处理呢?
首先,我们要做的就是状态机的校验,在我们的支付成功处理和超时关闭的处理过程中,需要做状态的判断,只有支付中的状态才能执行这两个动作。并且数据库的update语句也需要做控制,即:
update pay_order set status = "PAY_SUCCESS",lock_version = lock_version + 1 where pay_order_no = #{parOrderNo} and status = "PAYING" and lock_version = #{lock_version}
update pay_order set status = "PAY_EXPIRED",lock_version = lock_version + 1 where pay_order_no = #{parOrderNo} and status = "PAYING" and lock_version = #{lock_version}
这时候在发生并发时,就可以确保一个payorder,要么被推进到PAYSUCCESS状态,要么被推进到PAY_EXPIRED状态,会且只会发生其中的一种情况。
那有一个成功了,就有一个会失败。这是必然的,这时候就有两种情况了:
1、支付成功处理成功,支付超时处理失败
2、支付超时处理成功,支付成功处理失败
这两种情况如何处理呢?
先说简单的情况,假如支付成功处理成功,支付超时处理失败,这种其实没啥问题,因为已经支付成功了,超时的请求直接拒绝掉就行了。这是业务上正常的逻辑。
第二种情况就不好处理了,因为对于支付超时处理成功了,但是支付成功处理失败这种,我们就需要考虑,钱怎么办?
用户把钱付完了,但是支付却没成功,这肯定是业务上接受不了的。那这时候怎么办呢?
办法就是:原路退回
当出现这种情况的时候,我们是可以识别出来的,也就是说在支付成功的处理过程中,如果发现支付单被关闭了,那么就触发原路退回的流程,把钱再给用户退回去。
为啥非要退款?而不是让订单推进到成功,或者再补一个支付单。
一方面,状态机中已取消一定是一个终态,终态再流转到其他状态不合理。
另一方面,在订单超时的业务逻辑中,可能直接把库存退回去了,营销券也释放了,那么这时候补一个支付单是不现实的。
并且这个过程需要考虑,如果退回失败了怎么办?
为了保证方案的完整性,就需要在支付单上多记录一些金额,比如支付金额、冲退金额。当支付单的状态为PAY_SUCCESS时,需要满足以下恒等式:
支付金额 > 0 ;冲退金额=0
当支付单的状态为PAY_EXPIRED时,需要满足以下恒等式:
支付金额-冲退金额 = 0
只有这样,我们才能确保,这笔单子,要么用户付了钱,支付成功。要么支付失败,钱没付或者钱付了之后退回去了。
那我们就需要引入对账机制,不断的做资金恒等式的校验,发现不一致的时候,需要进行对齐操作,即退款重试。直到成功为止。
上面的方案大致差不多了,如果再做得好一点的话,可以在进入支付成功/支付超时的业务逻辑操作之前,先抢一下这笔支付单的锁,在前面就把并发控制好。没抢到锁的就失败,下次重试。
这样可以大大的降低并发冲突,避免出现异常,及很多线上的报错。