- 为什么会出现重复支付
重复支付的表象是同一笔订单被支付了多次。
过程是一笔订单已经支付了,在无结果返回的时候,又允许支付了下一笔订单,造成扣款多次。
在支付交易里,返回的结果不只有预料中的成功或失败,也会因为各种问题(如系统异常)导致收不到支付服务提供商反馈的结果。
但是交易订单必须有一个最终时间,不能无限期地等待下去,用户也不可能一直看着自己的订单在处理中,不知道购买是成功还是失败。认为订单成功,没付钱怎么办;认为不成功,要求重新支付,那就发生了重复支付;或者客户心急,多点了几次支付,多扣了钱怎么办。
解决之类问题需要使用补偿机制进行解决。
补偿机制:交易无结果,没有明确返回响应时,通过查询、冲正、退货等措施明确交易结果,以及对明确结果的处理机制。
查询是另一种对于交易结果未知的补偿机制。系统对于无明确交易结果的返回的订单,设定好脚本规则,定时向支付服务提供商发起请求,查询交易结果,比如每5分钟查询一次,一直查询到第30分钟。在这期间,如果查询到明确结果成功或者失败,更新订单状态;如果查到最后还是没有结果,通常的做法是直接置为失败,第二天商户查看对账单该交易是否成功,如果成功,则进行退款处理。
冲正是系统对于交易结果未知的补偿机制。商户因为系统超时、异常等,不确定支付结果,为避免用户等待或者重复扣款,向支付服务提供商发起冲正交易请求,进行交易回滚。无论原交易是成功还是失败,均要求取消该笔交易。冲正成功后,商户后续可以进行反馈用户支付失败或者再组织报文重新发起交易。
冲正与撤销、退货看起来有些相似,但是使用起来有很大区别:冲正可以对未知结果的订单进行交易回滚,而撤销和退货都只能对明确结果成功的订单进行交易回滚。
- 服务端如何防止重复支付
如图是一个简化的下单流程,首先是提交订单,然后是支付。支付的话,一般是走支付网关(支付中心),然后支付中心与第三方支付渠道(微信、支付宝、银联)交互,支付成功以后,异步通知支付中心,支付中心更新自身支付订单状态,再通知业务应用,各业务再更新各自订单状态。
这个过程中经常可能遇到的问题是掉单,无论是超时未收到回调通知也好,还是程序自身报错也好,总之由于各种各样的原因,没有如期收到通知并正确的处理后续逻辑等等,都会造成用户支付成功了,但是服务端这边订单状态没更新,这个时候有可能产生投诉,或者用户重复支付。
由于③⑤造成的掉单称之为外部掉单,由④⑥造成的掉单我们称之为内部掉单
为了防止掉单,这里可以这样处理:
1、支付订单增加一个中间状态“支付中”,当同一个订单去支付的时候,先检查有没有状态为“支付中”的支付流水,当然支付(prepay)的时候要加个锁。支付完成以后更新支付流水状态的时候再讲其改成“支付成功”状态。
2、支付中心这边要自己定义一个超时时间(比如:30秒),在此时间范围内如果没有收到支付成功回调,则应调用接口主动查询支付结果,比如10s、20s、30s查一次,如果在最大查询次数内没有查到结果,应做异常处理
3、支付中心收到支付结果以后,将结果同步给业务系统,可以发MQ,也可以直接调用,直接调用的话要加重试(比如:SpringBoot Retry)
4、无论是支付中心,还是业务应用,在接收支付结果通知时都要考虑接口幂等性,消息只处理一次,其余的忽略
5、业务应用也应做超时主动查询支付结果
对于上面说的超时主动查询可以在发起支付的时候将这些支付订单放到一张表中,用定时任务去扫
为了防止订单重复提交,可以这样处理:
1、创建订单的时候,用订单信息计算一个哈希值,判断redis中是否有key,有则不允许重复提交,没有则生成一个新key,放到redis中设置个过期时间,然后创建订单。其实就是在一段时间内不可重复相同的操作
附上微信支付最佳实践: