钱扣了,订单却是未支付,用户炸了——聊聊如何防止支付掉单

大家好,我是老三,之前在 如何防止订单重复支付 里和大家聊过掉单导致的重复支付,这篇文章,我们来聊聊,如何防止掉单。

好好的支付,怎么就掉单了?

我听说过下单、买单、脱单……掉单是什么东西?

所谓的掉单,就是用户下单支付,在钱包里完成了支付,结果回到电商APP一看,订单还是未支付……

毫无疑问,用户肯定会炸,结果不是客诉,就是差评。

用户感觉受到了欺诈

那么掉单是怎么来的呢?

我们先来看看订单支付的完整流程:

钱包支付的完整流程

  1. 用户从电商应用点击支付,客户端向服务端发起支付请求
  2. 支付服务会向第三方的支付渠道发起支付,支付渠道会响应对应的url
  3. 以APP为例,客户端通常是会拉起对应的钱包,用户跳到对应的钱包
  4. 用户在钱包里完成支付
  5. 用户完成支付后,跳转回对应的电商APP
  6. 客户端轮询订单服务,获取订单状态
  7. 支付渠道回调支付服务,通知支付结果
  8. 支付服务通知订单服务,更新订单状态

对于支付订单而言,大概可以分为这么几个状态:

支付状态

  • 未支付:用户在点击支付之后,支付服务请求支付渠道之前,处于未支付状态
  • 支付中:用户发起支付后,到跳转到支付钱包,再到完成支付,支付服务获取到最终支付结果之间,属于支付中状态,这个状态下,可以说是一个迷雾状态,电商系统对于用户的支付是不确定
  • 支付成功/失败/取消/关闭:电商系统最终确定了用户在第三方钱包的支付最终结果

看起来没什么问题啊,怎么就掉单了?简单说,就是支付的状态没有同步到,或者没有及时同步到。

掉单发生

  1. 支付渠道的支付回调

    发生了一些异常,导致支付服务没有收到支付渠道的回调通知

  2. 支付服务通知订单服务

    服务内部出现异常,导致支付状态没有同步到订单服务

  3. 客户端获取订单状态

    客户端通常是轮询获取状态,可能会在轮询时间内没有获取到订单状态,结果用户看到未支付

其中1可以称之为外部掉单,2和3可以称之为内部掉单。

接下来我们看看,怎么预防掉单问题。

怎么防止内部掉单

我们先从系统内部的掉单说起,当然在系统内部,稳定性更容易保证,发生掉单的概率还是比较小的。

服务端防止掉单

支付服务和订单服务之间防止掉单,关键就在于尽可能保证支付通知订单支付结果成功,我们一般通过这两种方式。

服务端防止掉单

  1. 同步调用重试机制

    支付服务调用订单服务的时候,要进行失败重试,防止网络抖动情况下的调用失败。

  2. 异步消息可靠性投递

    同步不稳妥,那就再加一个异步。支付服务投递一个支付成功消息,订单服务消费支付成功消息,整个过程要尽可能保证可靠性,例如订单服务要在完成订单状态更新后再确认完成消息消费。

同步+异步两手策略,基本上可以防范服务端的内部掉单。

至于引入分布式事务(事务消息、Seata)来保证状态一致,我觉得也没有必要。

客户端如何防止掉单

用户支付完成后,跳回电商系统,客户端会轮询一下订单的状态,通常两三秒内,就会得到订单完成支付的结果,这个过程出现问题的概率相比是非常低的。

但是也不排除,很小概率下,客户端轮询一段时间,还没得到结果,那么只能结束轮询,给用户展示未支付。

这种情况,通常问题也是出在服务端,没有及时更新订单的状态,最主要的还是要处理服务端的掉单,保证服务端能及时同步支付订单的状态。

但是一旦服务端的订单状态变更了,也要尽可能同步到客户端,不能让用户一直看到未支付。

客户端和服务端之间,同步状态,无非就是推和拉:

  1. 客户端轮询

    客户端判断用户未支付之后,通常会进行订单倒计时。

    倒计时

    这里再提一下?大家觉得这种倒计时是怎么实现的呢?纯客户端组组件倒计时吗?

    ——肯定不行,通常是客户端组件倒计时,定期向服务端请求,检查倒计时时间。同样的,这种情况下,客户端也可以检查支付状态。

  2. 服务端推送

    说真的,服务端推送,看上去是一种很美好的方案,Web端可以使用Websocket,APP端可以用自定义Push,大家可以看看我有 7种 实现web实时消息推送的方案,7种!。但实际上,推送的成功率经常不那么理想。

怎么防止外部掉单

相比较内部掉单,外部掉单发生的概率就大很多,毕竟和外部渠道的对接,不可控的因素更多。

要防止外部掉单,核心就是四个字:“主动查询”,如果只是等待第三方的回调通知,风险还是比较大的,支付服务要主动向第三方查询支付状态,即使有什么异常,也能及时感知到。

主动查询,主要就是两种形式:

定时任务查询

毫无疑问,最简单的肯定就是定时任务了,支付服务,定时查询一段时间内支付中的支付订单,向第三方渠道查询支付结果,查询到终态之后,就去更新支付订单状态、通知订单服务:

定时查询支付状态

实现也很简单,用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;}

定时任务的最大好处肯定是简单了,但是它也有一些问题:

  1. 查询的结果不实时

    定时任务频率的设置永远是个不好确定的事情,间隔短对数据库压力大,间隔长了不实时,很容易出现,上面提到的用户回到APP,结果轮询不到支付成功状态的情况。

    实际上,用户跳转钱包之后,通常会很快完成支付,如果短时间内没有完成支付,那么一般也不会再付了。所以其实,发起支付开始,从第三方查询支付结果的频率应该是递减的。

  2. 对数据库有压力

    定时任务扫表,对数据库肯定是会有压力的,扫表的时候,经常会看到数据库的监控出现一个小突刺,如果数据量大的话,可能影响更大。

    可以单独创建一个支付中流水表,定时任务扫描这张表,获取到支付最终态之后,就删除掉对应的记录。

延时消息查询

定时任务存在一些问题,那么有没有什么其它办法呢?答案是延时消息。

延时消息查询支付状态

  • 在发起支付之后,发送一个延时消息,前面讲到,用户跳转到钱包,通常很快会支付,所以我们希望查询支付状态这个步骤,符合这个规律,所以希望在10s、30s、1min、1min30s、2min、5min、7min……这种频率去查询支付订单的状态,这里我们可以用一个队列结构实现,队列里存放下一次查询的时间间隔。

    大概代码如下:

            //……//控制查询频率的队列,时间单位为sDeque<Integer> queue = new LinkedList<>();queue.offer(10);queue.offer(30);queue.offer(60);//……//支付订单号PaymentConsultDTO paymentConsultDTO = new PaymentConsultDTO();paymentConsultDTO.setPaymentId(paymentId);paymentConsultDTO.setIntervalQueue(queue);//发送延时消息Message message = new Message();message.setTopic("PAYMENT");message.setKey(paymentId);message.setTag("CONSULT");message.setBody(toJSONString(paymentConsultDTO).getBytes(StandardCharsets.UTF_8));try {//第一个延时消息,延时10slong delayTime = System.currentTimeMillis() + 10 * 1000;// 设置消息需要被投递的时间。message.setStartDeliverTime(delayTime);SendResult sendResult = producer.send(message);//……} catch (Throwable th) {log.error("[sendMessage] error:", th);}
    

    PS:这里用的是RocketMQ云服务器版,支持任意级别的延时消息,开源版的RocketMQ只支持固定级别的延时消息,不得不感概充钱才能变强。有实力的开发团队,可以在开源基础上,进行二次开发。

  • 在消费到延时消息之后,向第三方查询支付订单的状态,如果还在支付中,就继续发送下一个延时消息,延时间隔从队列结构中取。如果获取到最终态,就去更新支付订单状态、通知订单服务。

    @Component
    @Slf4j
    public class ConsultListener implements MessageListener {//消费者注册,监听器注册//……@Overridepublic Action consume(Message message, ConsumeContext context) {// UTF-8解析String body = new String(message.getBody(), StandardCharsets.UTF_8);PaymentConsultDTO paymentConsultDTO= JsonUtil.parseObject(body, new TypeReference<PaymentConsultDTO>() {});if (paymentConsultDTO == null) {return Action.ReconsumeLater;}//获取支付流水PayDO payDO=payMapper.selectById(paymentConsultDTO.getPaymentId());//……//查询支付状态PaymentStatusResult paymentStatusResult=payService.getPaymentStatus(paymentStatusContext);//还在支付中,继续投递一个延时消息if (PaymentStatusEnum.PENDING.equals(paymentStatusResult.getPayStatus())){//发送延时消息Message msg = new Message();message.setTopic("PAYMENT");message.setKey(paymentConsultDTO.getPaymentId());message.setTag("CONSULT");//下一个延时消息的频率Long delaySeconds=paymentConsultDTO.getIntervalQueue().poll();        message.setBody(toJSONString(paymentConsultDTO).getBytes(StandardCharsets.UTF_8));try {Long delayTime = System.currentTimeMillis() + delaySeconds * 1000;// 设置消息需要被投递的时间。message.setStartDeliverTime(delayTime);SendResult sendResult = producer.send(message);//……} catch (Throwable th) {log.error("[sendMessage] error:", th);}return Action.CommitMessage;}//获取到最终态//更新支付订单状态//…… //通知订单服务//……return Action.CommitMessage;}
    }
    

    延时消息的方案相对于定时轮询方案来讲:

    • 时效性更好
    • 无需扫表,对数据库压力较小

    不过大家也看到,我这里的实现是利用的是充钱版的RocketMQ,所以看起来不太复杂,但是如果用开源方案,那就没那么简单。

    充钱就能解决

结语

这篇文章介绍了一个让用户炸毛,让客服恼火,让开发挠头的问题——掉单,包括为什么会掉单,怎么防止掉单。

其中内部掉单,发生的概率相对较少,掉单最主要的原因还是所谓的外部掉单。

外部掉单解决的关键点是主动查询,有两种常用的方案:定时任务查询延时消息查询,前者简单一些,后者功能上更加出色。



参考:

[1]. 支付掉单异常最全解决方案

[2]. 解决支付掉单问题


⭐面渣逆袭系列:

  • 面渣逆袭:Java基础五十三问
  • 面渣逆袭:Java集合连环三十问
  • 面渣逆袭:JVM经典五十问,这下面试稳了!
  • 面渣逆袭:Java并发六十问
  • 面渣逆袭:Spring三十五问,四万字+五十图详解!
  • 面渣逆袭:二十二图、八千字、二十问,彻底搞定MyBatis!
  • 面渣逆袭:计算机网络六十二问,三万字图文详解!速收藏!
  • 面试字节,被操作系统问挂了
  • 面渣逆袭:RocketMQ二十三问

关注⬇️⬇️⬇️,回复「666」,领取七百多页独家原创的面试手册!

面渣逆袭手册

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/31212.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

微信支付最佳实践-服务端如何防止重复支付

DUBBO之家 2023-01-10 14:01 发表于北京 点击上方 "DUBBO之家" 关注公众号 终身学习 技术干货 及时送达 往期回顾 这次彻底读透 Redis Java回调机制 概述 如图是一个简化的下单流程&#xff0c;首先是提交订单&#xff0c;然后是支付。支付的话&#xff0c;一…

如何防止订单重复支付

想必大家对在线支付都不陌生&#xff0c;今天和大家聊聊如何防止订单重复支付。 看看订单支付流程 我们来看看&#xff0c;电商订单支付的简要流程&#xff1a; 订单钱包支付流程 从下单/计算开始&#xff1a; 下单/结算&#xff1a;这一步虽然不是直接的支付起点&#xff0c;但…

重复付款异常到底该如何解决?一笔订单,但是误付了两笔钱!

重复付款异常 异常场景 重复付款异常一般常见于网银支付&#xff0c;微信支付&#xff0c;支付宝等这类需要跳转到一个支付网关页&#xff08;网银支付&#xff09;&#xff0c;或者跳转到钱包 APP(支付宝、微信)&#xff0c;从而异步完成扣款的支付场景。 这种支付场景下&a…

《GPT-4 ,通用人工智能的火花》论文内容精选与翻译

原文&#xff1a; 原文地址&#xff1a;Arduino中文社区 引言&#xff1a; 《通用人工智能的火花&#xff1a;GPT-4早期实验》是3月最重要的一篇论文&#xff0c;引起了广泛的关注和讨论&#xff0c;但是论文长达 154页&#xff0c;中文版本还无人翻译。 本文挑选了论文中的…

ICLR 2023 Oral | Batch Norm层等暴露TTA短板,开放环境下解决方案来了

关注并星标 从此不迷路 计算机视觉研究院 公众号ID&#xff5c;ComputerVisionGzq 学习群&#xff5c;扫码在主页获取加入方式 计算机视觉研究院专栏 作者&#xff1a;Edison_G 测试时自适应&#xff08;Test-Time Adaptation, TTA&#xff09;方法在测试阶段指导模型进行快速无…

TensorRT量化第二课:对称量化与非对称量化

目录 模型量化原理注意事项一、2023/3/30更新前言1.引出问题1.1 问题1.2 代码实现1.2.1 初始化输入数组1.2.2 Scale计算1.2.3 量化截断1.2.4 反量化1.2.5 完整代码 2. 非对称量化2.1 动态范围量化2.2 代码实现2.3 原理分析2.3.1 动态量化范围2.3.2 偏移量Z2.3.3 图例分析 3.对称…

领先的项目协作管理软件OpenProject

本文软件由网友 不长到一百四誓不改名 推荐&#xff1b; 什么是 OpenProject &#xff1f; OpenProject 是一个开源、基于 Web 的项目管理系统&#xff0c;提供了免费的社区版和收费的企业版。OpenProject 拥有完善的文档&#xff0c;API&#xff0c;及丰富的功能&#xff0c;可…

当我问ChatGPT,知识图谱在工程项目管理中有什么用

导读 知识图谱&#xff08;Knowledge Graph&#xff09;&#xff0c;在图书情报界称为知识域可视化或知识领域映射地图&#xff0c;是显示知识发展进程与结构关系的一系列各种不同的图形&#xff0c;用可视化技术描述知识资源及其载体&#xff0c;挖掘、分析、构建、绘制和显示…

私藏多年的vscode插件分享,让你成为一个高效开发的程序员

vscode插件就像手机里的应用商店一样&#xff0c;可以让我们在高效代码开发、为了美观的代码格式&#xff0c;可以更好的高逼格分享代码等系列功能&#xff0c;本文特意整理了艾编程老师多年来使用vscode的经验&#xff0c;整理的插件集希望对您有帮助&#xff01; 1、简体中文…

vscode插件(个人正在用的)

插件目录 any-ruleAuto Close TagAuto Rename Tagbackground-coverChinese (Simplified) (简体中文) Language Pack for Visual Studio CodeDebugger for JavaError LensESLintExtension Pack for JavaImage previewIntelliCodeIntelliCode API Usage ExamplesLanguage Support…

程序员请收好:10个非常有用的 Visual Studio Code 插件!

点击上方“小白学视觉”&#xff0c;选择加"星标"或“置顶” 重磅干货&#xff0c;第一时间送达 一个插件列表&#xff0c;可以让你的程序员生活变得轻松许多。 以下为译文&#xff1a; 无论你是经验丰富的开发人员还是刚刚开始第一份工作的初级开发人员&#xff0c;…

VSCode好用的插件

文章の目录 1、Chinese (Simplified) (简体中文) Language Pack&#xff08;汉化vscode 必备&#xff09;2、Bracket Pair Colorizer&#xff08;给代码中的括号添加亮色&#xff0c;便于区分&#xff09;3、Auto Close Tag&#xff08;自动补全标签&#xff0c;必备&#xff0…

30个实用VSCode 插件,让你的开发效率倍增!

1. Image preview 通过此插件&#xff0c;当鼠标悬浮在图片的链接上时&#xff0c;可以实时预览该图片&#xff0c;除此之外&#xff0c;还可以看到图片的大小和分辨率。 2. Auto Rename Tag 使用该插件&#xff0c;可以在重命名一个 HTML 标签时&#xff0c;自动重命名 HTML…

跟我做一个可以聊天的 Visual Studio Code 插件

你每天有在用 Visual Studio Code 吗&#xff1f; 根据面向程序开发人员的时间跟踪工具 WakeTime 统计 &#xff0c; 在 2020 年全球开发者使用 Visual Studio Code 的时间合共 1800 万小时 。这是一个非常惊人的数字 。 你有想过开发一个 Visual Studio Code 插件吗 &#xff…

vscode常用的9个插件,推荐给你们

1. Settings Sync 开发必备神器之一&#xff01;可以帮助你在不同的设备之间同步vscode所有的配置、插件&#xff01;&#xff01;! 虽然配置有好几个步骤&#xff0c;但是一旦配置好了之后使用非常的方便&#xff0c;只需要记住快速上传和快速下载的快捷键即可。甚至你可以选…

VS Code实用插件推荐

一、外观优化插件 1.1 Chinese 中文插件包&#xff0c;看起来清晰明了&#xff0c;安装完重启vs即可生效&#xff1b; 1.2 Better Comments 一款美化注释的插件&#xff0c;可以根据不同种类的注释&#xff0c;显示不同的颜色&#xff0c;一目了然。还可以通过扩展配置文件…

VSCode插件推荐

1. VSCode汉化包插件 &#xff1a;Chinese (Simplified) (简体中文) Language VSCode汉化包&#xff0c;原始默认是英文的所以我们需要下一个中文插件。 2. VSCode自动补全标签 &#xff1a;Auto Close Tag Auto Close Tag 对Html或Xml文件自动创建结束标签&#xff1b; 如在…

VScode神仙插件,程序员必备

前言 Visual Studio Code(VS Code)是微软2015年推出的一个轻量但功能强大的源代码编辑器&#xff0c;基于 Electron 开发&#xff0c;支持 Windows、Linux 和 macOS 操作系统。它内置了对JavaScript&#xff0c;TypeScript和Node.js的支持并且具有丰富的其它语言和扩展的支持&a…

VSCode提高代码开发效率插件:(一)差异对比插件

写代码经常会用到代码对比的功能&#xff0c;以前常用独立的软件Merge&#xff0c;Vscode中也有类似功能的插件。之前开发单片机一直用的Keil&#xff0c;但是用Keil编译去掉BroseInformation速度提上来了但是没法函数跳转了。 Vscode可以解决这个问题了。SourceInsight之类的代…

VScode 常用插件推荐,非常全面

文章目录 一、主题美化PeacockMaterial ThemeMaterial Theme IconsbackgroundPower ModeRainbow CSVIndent RainbowPolacode 二、检查格式化ESLintPrettier - Code formatterPrettier ESLintStylelint 三、编程美化Document ThisBetter CommentsRainbow Brackets 四、集成插件D…