好好的支付,怎么就掉单了?
我听说过下单、买单、脱单……掉单是什么东西?
所谓的掉单,就是用户下单支付,在钱包里完成了支付,结果回到电商APP一看,订单还是未支付……
毫无疑问,用户肯定会炸,结果不是客诉,就是差评。
那么掉单是怎么来的呢?
我们先来看看订单支付的完整流程:
- 用户从电商应用点击支付,客户端向服务端发起支付请求
- 支付服务会向第三方的支付渠道发起支付,支付渠道会响应对应的url
- 以APP为例,客户端通常是会拉起对应的钱包,用户跳到对应的钱包
- 用户在钱包里完成支付
- 用户完成支付后,跳转回对应的电商APP
- 客户端轮询订单服务,获取订单状态
- 支付渠道回调支付服务,通知支付结果
- 支付服务通知订单服务,更新订单状态
对于支付订单而言,大概可以分为这么几个状态:
- 未支付:用户在点击支付之后,支付服务请求支付渠道之前,处于未支付状态 支付中:用户发起支付后,到跳转到支付钱包,再到完成支付,支付服务获取到最终支付结果之间,属于支付中状态,这个状态下,可以说是一个迷雾状态,电商系统对于用户的支付是不确定 支付成功/失败/取消/关闭:电商系统最终确定了用户在第三方钱包的支付最终结果
看起来没什么问题啊,怎么就掉单了?简单说,就是支付的状态没有同步到,或者没有及时同步到。
- 支付渠道的支付回调 发生了一些异常,导致支付服务没有收到支付渠道的回调通知
- 支付服务通知订单服务 服务内部出现异常,导致支付状态没有同步到订单服务
- 客户端获取订单状态 客户端通常是轮询获取状态,可能会在轮询时间内没有获取到订单状态,结果用户看到未支付
其中1可以称之为外部掉单,2和3可以称之为内部掉单。
接下来我们看看,怎么预防掉单问题。
怎么防止内部掉单
我们先从系统内部的掉单说起,当然在系统内部,稳定性更容易保证,发生掉单的概率还是比较小的。
服务端防止掉单
支付服务和订单服务之间防止掉单,关键就在于尽可能保证支付通知订单支付结果成功,我们一般通过这两种方式。
- 同步调用重试机制 支付服务调用订单服务的时候,要进行失败重试,防止网络抖动情况下的调用失败。
- 异步消息可靠性投递 同步不稳妥,那就再加一个异步。支付服务投递一个支付成功消息,订单服务消费支付成功消息,整个过程要尽可能保证可靠性,例如订单服务要在完成订单状态更新后再确认完成消息消费。
同步+异步两手策略,基本上可以防范服务端的内部掉单。
至于引入分布式事务(事务消息、Seata)来保证状态一致,我觉得也没有必要。
客户端如何防止掉单
用户支付完成后,跳回电商系统,客户端会轮询一下订单的状态,通常两三秒内,就会得到订单完成支付的结果,这个过程出现问题的概率相比是非常低的。
但是也不排除,很小概率下,客户端轮询一段时间,还没得到结果,那么只能结束轮询,给用户展示未支付。
这种情况,通常问题也是出在服务端,没有及时更新订单的状态,最主要的还是要处理服务端的掉单,保证服务端能及时同步支付订单的状态。
但是一旦服务端的订单状态变更了,也要尽可能同步到客户端,不能让用户一直看到未支付。
客户端和服务端之间,同步状态,无非就是推和拉:
- 客户端轮询 客户端判断用户未支付之后,通常会进行订单倒计时。 这里再提一下?大家觉得这种倒计时是怎么实现的呢?纯客户端组组件倒计时吗? ——肯定不行,通常是客户端组件倒计时,定期向服务端请求,检查倒计时时间。同样的,这种情况下,客户端也可以检查支付状态。
- 服务端推送 说真的,服务端推送,看上去是一种很美好的方案,Web端可以使用Websocket,APP端可以用自定义Push,大家可以看看。但实际上,推送的成功率经常不那么理想。
怎么防止外部掉单
相比较内部掉单,外部掉单发生的概率就大很多,毕竟和外部渠道的对接,不可控的因素更多。
要防止外部掉单,核心就是四个字:“主动查询”,如果只是等待第三方的回调通知,风险还是比较大的,支付服务要主动向第三方查询支付状态,即使有什么异常,也能及时感知到。
主动查询,主要就是两种形式:
定时任务查询
毫无疑问,最简单的肯定就是定时任务了,支付服务,定时查询一段时间内支付中的支付订单,向第三方渠道查询支付结果,查询到终态之后,就去更新支付订单状态、通知订单服务:
实现也很简单,用xxl-job之类的定时任务框架,定时扫表,向第三方查询就行了,大概代码如下:
@XxlJob("syncPaymentResult") public ReturnT<String> syncPaymentResult(int hour) { //…… //查询一段之间支付中的流水 List<PayDO> pendingList = payMapper.getPending(now.minusHours(hour)); for (PayDO payDO : pendingList) { //…… // 主动去第三方查 PaymentStatusResult paymentStatusResult = paymentService.getPaymentStatus(paymentId); // 第三方支付中 if (PaymentStatusEnum.PENDING.equals(paymentStatusResult.getPayStatus())) { continue; } //支付完成,获取到终态 //…… // 1.更新流水 payMapper.updatePayDO(payDO); // 2.通知订单服务 orderService.notifyOrder(notifyLocalRequestVO); } return ReturnT.SUCCESS; }
定时任务的最大好处肯定是简单了,但是它也有一些问题:
- 查询的结果不实时 定时任务频率的设置永远是个不好确定的事情,间隔短对数据库压力大,间隔长了不实时,很容易出现,上面提到的用户回到APP,结果轮询不到支付成功状态的情况。 实际上,用户跳转钱包之后,通常会很快完成支付,如果短时间内没有完成支付,那么一般也不会再付了。所以其实,发起支付开始,从第三方查询支付结果的频率应该是递减的。
- 对数据库有压力 定时任务扫表,对数据库肯定是会有压力的,扫表的时候,经常会看到数据库的监控出现一个小突刺,如果数据量大的话,可能影响更大。 可以单独创建一个支付中流水表,定时任务扫描这张表,获取到支付最终态之后,就删除掉对应的记录。
延时消息查询
定时任务存在一些问题,那么有没有什么其它办法呢?答案是延时消息。
原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/290676.html