太稳了,支付系统就该这么设计

news2024/11/22 15:19:22

支付中心系统对内为各个业务线提供统一的支付、退款等服务,对外对接三方支付或银行服务实现资金的流转。如下图:

大部分公司基本都是这样的架构,主要有以下几方面的优点:

  1. 形成统一支付服务,降低业务线接入成本及重复研发成本。

  2. 更好更快的支持创新业务,为公司业务快速发展提供条件。

  3. 更利于构建安全,稳定,可扩展的支付系统。

  4. 利于核心支付数据的沉淀和统一利用。

支付流程

上图展示了用户支付的主要流程,分为三个步骤:

  1. 用户在业务订单确认页,唤起收银台页面。

  2. 用户在收银台页面选择支付方式,确认支付,显示第三方支付页面,输入密码,进行真实支付行为。

  3. 系统处理用户支付结果,并通知给用户及各个相关系统。

下面详细说下这三个步骤:

1. 唤起商户收银台

  1. 用户在订单确认页点击“去支付“按钮,调用收银台支付下单接口。

  2. 收银台将订单信息缓存并入库,然后将订单标识拼装到收银台URL上返回给订单系统。

  3. 订单系统接收到收银台地址跳转到收银台页面。

上图展示了两个业务线(景区业务线,酒店业务线)唤起的收银台页面,大概可以分为三个区域:

页面上部分显示的是支付剩余时间和应付金额;

中间部分是订单信息,根据收银台定义的数据格式,业务线动态传递过来的;

剩余部分展示的是支付渠道,支付渠道也是业务线根据自己的需求在支付后台管理系统配置的,想要哪些支付方式以及它们的顺序都可以自定义。

2. 用户确认支付

  1. 用户在收银台页面选择支付方式(支付宝支付,微信支付,银行卡支付等),点击立即支付按钮,

  2. 调用支付中心创单接口,支付中心调用三方支付创单接口,同步返回支付信息,支付中心对返回参数进行处理,返回给收银台,

  3. 收银台携带支付中心返回的参数,调用三方接口,唤起三方收银台,

  4. 用户输入密码,立即支付。

3. 支付结果处理

  1. 三方系统进行扣款处理,返回收银台结果(目前微信支付返回支付中,支付宝返回支付终态(支付成功或支付失败)),

以下几个步骤是异步执行的,不分先后。

  1. 收银台拿到三方返回的结果,确认用户已经支付,则分配定时任务轮询查询(注意超时时间)后台支付结果,拿到终态之后跳转到相应页面,

  2. 三方系统支付成功后会通知支付中心结果,支付中心做好自身逻辑处理,异步通知订单系统,然后返回三方系统通知结果,

  3. 如果长时间未收到三方支付结果的通知,为了防止掉单,支付中心会发起主动查询来获取支付最终结果,以保证支付结果的及时更新。

  4. 当然业务线订单系统为了防止支付系统出现异步通知问题,也可以定时轮询支付中心的支付状态,防止掉单。(图中未画)

支付中心系统一些问题及解决方案

1. 支付订单超时关闭问题

如果用户长时间没有支付,一般都会有一个超时时间(如上图商户收银台的支付剩余时间),到达这个超时时间支付单会自动关闭。实现此需求有很多方式,比如:

1. 轮询 DB

定时轮询DB,取出达到超时时间且在支付中的数据,然后执行关闭逻辑。

缺点:1. 存在延迟,取决于定时任务的频率。2. 影响数据库性能。

2. JDK 延时队列(DelayQueue)和时间轮算法

这两种的算法的实现方式自行搜索。

共同的缺点是 1. 数据易丢失,由于数据存储在内存中,服务重启后数据全部消失。2. 有内存限制,如果数据量过大,会出现OOM异常。

3. RocketMQ 延时队列

RocketMQ 支持消息延时发送,社区版不支持任意等级的延迟,目前默认支持18个延时等级:

1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h 

比如支付单30分钟过期,在支付单创建成功后发送延迟消息(延时等级为 16),消费者在30分钟后会拉取到该消息然后执行关闭逻辑。

RocketMQ 延时队列,无论在数据安全性和及时性都有明显的优势,但是目前社区版没有支持任意级别的延迟。

目前我们使用的是 RocketMQ 延时队列实现的订单关闭。

2. 保证支付结果实时性

三方支付系统支付成功后99.9%的情况下都会回调通知我们,但也难免有意外,比如三方延迟回调或者三方系统宕机,为了保证支付结果的实时性,三方支付也要求我们不能完全依赖于回调接口,所以我们需要定时的调用主动查询接口来查询三方的支付结果。这里我们也是使用的 RocketMQ 延时队列实现的:

  1. 调用三方支付创单成功后,发送<支付主动查询>延时MQ消息。

  2. 消费消息,判断支付状态是否到达终态,如果到达终态,则返回处理成功,否则调用三方支付查询接口,如果支付成功则处理成功业务,返回处理成功。

  3. 如果客户未支付则判断是否达到最大的重试次数,如果达到最大重试次数则停止<支付主动查询>的重试,否则解析重试规则,发送下一轮的延时消息。

有三个重要参数,这些参数可以放到配置中心或者配置库中,

// 初始延迟级别,对应RocketMQ延时等级,比如3对应的延时时间就是10s
private Integer queryInitLevel = 3;

// 重试次数
private Integer queryCount = 6;

// 重试级别,对应RocketMQ延时等级,5s,10s,30s,1m,10m,20m
private String queryDelayLevels = 2,3,4,5,14,15;

支付创单成功后发送延时消息:

public void payQueryTask(String orderNo) {
        PayQueryMessage payQueryMessage = new PayQueryMessage();
        payQueryMessage.setOrderNo(orderNo);

        RetryMessage<PayQueryMessage> retryMessage = new RetryMessage<>();
        retryMessage.setTotalCount(queryCount);
        retryMessage.setDelayLevels(queryDelayLevels);
        retryMessage.setTopic(TopicConst.PAY_QUERY_TOPIC);
        retryMessage.setEventType(RetryEventTypeEnum.PAY_QUERY);
        retryMessage.setEventDesc(RetryEventTypeEnum.PAY_QUERY.getDesc());
        retryMessage.setData(payQueryMessage);

        log.info("{} - 发送消息, retryMessage: {}", LOG_DESC, retryMessage);
        rocketMqProducer.asyncSend(retryMessage.getTopic(), JsonUtil.toJson(retryMessage),
                CodeEnum.codeOf(RocketMQDelayLevelEnum.class, queryInitLevel).orElse(RocketMQDelayLevelEnum.FiveSeconds), LOG_DESC);
}

判断的是否继续执行任务:

public void sendDelayRetry(RetryMessage<?> retryMessage) {
        int currentCount;
        retryMessage.setCurrentCount(currentCount = retryMessage.getCurrentCount() + 1);
        // 重试达到最大次数
        if (currentCount > retryMessage.getTotalCount()) {
            log.warn("{} - 达到最大次数-{}, 停止重试! retryMessage: {}", retryMessage.getEventDesc(), retryMessage.getTotalCount(), JsonUtil.toJson(retryMessage));
            return;
        }
        log.info("{} - 发送重试消息-{}/{}, retryMessage: {}", retryMessage.getEventDesc(), retryMessage.getCurrentCount(), retryMessage.getTotalCount(), JsonUtil.toJson(retryMessage));
        int delayLevel = Integer.parseInt(retryMessage.getDelayLevels().split(",")[retryMessage.getCurrentCount() - 1]);
        rocketMqProducer.asyncSend(retryMessage.getTopic(), retryMessage,
                CodeEnum.codeOf(RocketMQDelayLevelEnum.class, delayLevel).orElse(RocketMQDelayLevelEnum.FiveSeconds), retryMessage.getEventDesc()+", 发送重试消息");
    }

3. 支付结果通知上游容错

在回调通知上游系统支付结果时,可能会回调失败,比如网络异常或上游系统发生短时故障,如果发生这种情况我们单靠简单的重试是无法完全解决问题的。为了尽可能的通知成功,我们需要针对没有通知成功的数据,每隔一段时间通知一次,那这个需求和我们上一个问题差不多,所以可以复用我们的延时重试框架。

流程和保证支付结果实时的差不多,不再赘述。

支付中心系统中设计模式的应用

模板方法

模板方法模式思想:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

简单的理解就是定义一个模版方法,然后子类实现模版方法中的抽象方法实现个性化的需求。

就支付而言,无论何种支付产品,都是走的同一个支付流程,那我们就可以定义一个支付流程的模板,然后每种支付产品实现这个模板中特定步骤来实现自己的特定需求。

策略

策略模式主要思想:定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。

在支付系统中,支付结果主动查询需要查询不同的渠道,比如支付宝,微信,银联等,每个渠道查询的方式和参数不尽相同,可以将每种渠道查询封装成不同的策略类,然后根据查询条件来调用不同的策略类。

查询策略有两个策略接口,callChannel功能是组装查询参数和查询三方,execute 是处理三方返回的结果统一为支付中心状态。(因callChannel有其他地方共用所以分开了两个方法)。

Spring 下使用策略模式,在项目启动时,将所有的策略类加载到Map中,然后使用时直接在Map中获取。

@Component
public class PayQueryStrategyContext {

    private final Map<String, PayQueryStrategy> payQueryStrategyMap = Maps.newConcurrentMap();

    public PayQueryStrategyContext(Map<String, PayQueryStrategy> payQueryStrategyMap) {
        this.payQueryStrategyMap.clear();
        payQueryStrategyMap.forEach(this.payQueryStrategyMap::put);
    }

    public PayQueryStrategy getPayQuery(@NotNull String channelCode) {
        return this.payQueryStrategyMap.get(OperationTypeConst.Pay_Query + channelCode);
    }
}

 最后

如果感觉本文对你有帮助,点赞关注支持一下,想要了解更多Java后端,大数据,算法领域最新资讯可以关注我公众号【架构师老毕】私信666还可获取更多Java后端,大数据,算法PDF+大厂最新面试题整理+视频精讲

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数据结构-树,森连,二叉树之间的转换

树》二叉树 1.给兄弟加线 2.给出长子外的孩子去线 3.层次调整 &#xff08;整体向左偏移45&#xff09; eg&#xff1a; 1.给兄弟加线&#xff1a; 2.给处长紫外的孩子去线 3.层次调整&#xff0c;整体向左偏移45 &#xff08;由兄弟转化来的孩子都是右节点&#xff0c…

DJ6-5 目录管理

目录 6.5.1 文件控制块和索引结点 1、文件控制块 FCB 2、索引节点 6.5.2 简单文件目录 1、单级目录结构 2、二级目录结构 3、树形目录结构 6.5.3 目录查询技术 1、线性检索法 2、Hash 方法 文件目录&#xff1a;是指由文件说明索引组成的用于文件检索的特殊文件…

chatgpt赋能python:Python文件复制到指定文件夹——实现简单又高效的文件操作

Python 文件复制到指定文件夹——实现简单又高效的文件操作 如今&#xff0c;人们对于数据的需求越来越多&#xff0c;因此在编程过程中&#xff0c;对于文件的操作也变得越来越重要。而Python作为一种高效而简洁的编程语言&#xff0c;其文件操作也是十分出色的。本文将会带领…

C/C++ ---- 内存管理

目录 C/C内存分布 常见区域介绍 经典习题&#xff08;读代码回答问题&#xff09; 选择题 填空题 C语言内存管理方式 malloc/free calloc realloc C内存管理方式 new和delete操作内置类型 new和delete操作自定义类型 operator new和operator delete函数 new和dele…

Linux环境变量总结

Linux是一个多用户的操作系统。多用户意味着每个用户登录系统后&#xff0c;都有自己专用的运行环境。而这个环境是由一组变量所定义,这组变量被称为环境变量。用户可以对自己的环境变量进行修改以达到对环境的要求。 设置环境变量的方法 对所有用户生效的永久性变量 这类变…

K8s进阶6——pod安全上下文、Linux Capabilities、OPA Gatekeeper、gvisor

文章目录 一、Pod安全上下文1.1 配置参数1.2 案例11.2.1 dockerfile方式1.2.2 pod安全上下文方式 1.3 案例21.4 Linux Capabilities方案案例1案例2 二、pod安全策略2.1 PSP&#xff08;已废弃&#xff09;2.1.1 安全策略限制维度 2.2 OPA Gatekeeper方案2.2.1 安装Gatekeeper2.…

百度搜索迎来奇点 大模型掀起代际变革

每一轮技术革命掀起的浪潮&#xff0c;大部多数人还没来得及思考或者布局&#xff0c;已经消失于海浪中。机会是给有准备的人的&#xff0c;要发现新兴技术的亮点&#xff0c;并立足自身去积极拥抱它&#xff0c;最后转化为自身前进的动力&#xff0c;跨越周期&#xff0c;迎来…

网站出现403 Forbidden错误的原因以及怎么解决的方法

这几天刚接手一批新做的网站&#xff0c;在访问网站的时候&#xff0c;会时不时的出现403 Forbidden错误&#xff0c;浏览器会给出403 Forbidden错误提示&#xff0c;在打开Access Error中列出的URL之后, 出现以下错误&#xff1a; 403 Forbidden Access to this resource on…

SAP工具箱 批量下载指定表数据到EXCEL

点击蓝字 关注我们 一 前言 下载指定表内容到指定的EXCEL是一个比较简单的程序.但仔细考虑这个程序,还是可以在细节上找出一些关注点 多表内容同时下载,每个表生成一个文件多表选择时,先查看表的记录数大表下载时,拆分下载拆分到不同的文件中拆分到同一个文件中的不同的工作表下…

windows server 2016 ftp搭建详细教程

一.什么是FTP&#xff1f; FTP(File Transfer Protocol)是TCP/IP网络上两台计算机传送文件的协议&#xff0c;使得主机间可以共享文件。 接下来我给大家分享快速搭建FTP服务器的方法。 二.安装FTP服务器 1.进入服务器系统打开“服务器管理器”&#xff0c;点击“添加角色和功…

【JavaSE】Java基础语法(二十三):递归与数组的高级操作

文章目录 1. 递归1.1 递归1.2 递归求阶乘 2. 数组的高级操作2.1 二分查找2.2 冒泡排序2.3 快速排序2.4 Arrays (应用) 1. 递归 1.1 递归 递归的介绍 以编程的角度来看&#xff0c;递归指的是方法定义中调用方法本身的现象把一个复杂的问题层层转化为一个与原问题相似的规模较…

C语言2:说心里话

描述 分两次从控制台接收用户的两个输入&#xff1a;第一个内容为“人名”&#xff0c;第一个内容为“心里 话”。 然后将这两个输入内容组成如下句型并输出出来&#xff1a; 1.(人名&#xff09;&#xff0c;I want to say&#xff0c;(心里话 2. 输入输出示例: 输入&#xff…

MybatisPlus SpringCloud Docker RabbitMQ ElasticSearch、Redis高级技术,分布式事务的综合应用

一、配置SpringCloud中的网关 1. nginx搭建 搭建好了启动nginx.exe即可出静态页面图 1.网关搭建 server:port: 10010 spring:application:name: api-gatewaycloud:nacos:server-addr: localhost:8848gateway:routes: #用户服务的路由&#xff1a;什么样的请求&#xff0c;让网…

Emacs之定制化mode line(第一百零二)

简介: CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长! 优质专栏:Audio工程师进阶系列【原创干货持续更新中……】🚀 人生格言: 人生从来没有捷径,只有行动才是治疗恐惧和懒惰的唯一良药. 更多原创,欢迎关注:Android…

第二期:链表经典例题(两数相加,删除链表倒数第N个节点,合并两个有序列表)

每道题后都有解析帮助你分析做题&#xff0c;答案在最下面&#xff0c;关注博主每天持续更新。 PS&#xff1a;每道题解题方法不唯一&#xff0c;欢迎讨论&#xff01; 1.两数相加 题目描述 给你两个非空的链表&#xff0c;表示两个非负的整数。它们每位数字都是按照逆序的方式…

【Vue】二:Vue核心处理---模板语法

文章目录 1.模板语法---插值2.模板语法---指令语法2.1v-once2.2 v-bind2.3 v-model2.4 v-on 3.MVVM4.事件回调函数中的this 1.模板语法—插值 {{可以写什么}} &#xff08;1&#xff09;在data中声明的变量&#xff0c;函数 &#xff08;2&#xff09;常量 &#xff08;3&…

【蓝桥杯省赛真题22】python剩余空间问题 青少年组蓝桥杯比赛python编程省赛真题解析

目录 python剩余空间问题 一、题目要求 1、编程实现 二、解题思路

【JavaEE】锁策略、CAS和synchronized的优化

目录 1、常见的锁策略 1.1、乐观锁 vs 悲观锁 1.2、轻量级锁 vs 重量级锁 1.3、自旋锁 vs 挂起等待锁 1.4、互斥锁 vs 读写锁 1.4.1、读写锁的使用场景&#xff08;适用于"频繁 读&#xff0c;不频繁写"的场景&#xff09; 1.5、可重入锁 vs 不可重入锁 1.…

计算机专业学习的核心是什么?

既然是学习CS&#xff0c;那么在这里&#xff0c;我粗浅的把计算机编程领域的知识分为三个部分&#xff1a; 基础知识 特定领域知识 框架和开发技能 基础知识是指不管从事任何方向的软件工程师都应该掌握的&#xff0c;比如数据结构、算法、操作系统。 特定领域知识就是你…

Python花瓣雨

目录 前言 小海龟 花朵类 移动函数 画花朵 尾声 前言 来啦来啦来啦&#xff0c;小伙伴们快快来领取七彩花瓣雨吧&#xff01;&#xff01; 小海龟 老生常谈啦&#xff0c;在用python画樱花树前&#xff0c;我们先来了解一下turtle吧&#xff01; 小海龟(Turtle)是P…