简易支付系统的架构设计

原创 2018年01月02日 10:03:33

作者:文刀(微信公众号:jishuhui_2015),Java Web全栈工程师,高级架构师,技术布道者。曾任两家上市公司的技术主管,从事微服务架构设计,DevOps团队建设工作,在电商、LBS、IoT等相关应用领域有丰富的项目经验。
责编:钱曙光(qianshg@csdn.net)

支付系统是一个老生常谈的话题,每个公司开发的支付系统不尽相同,因为业务形态并不太一样。

本文并不是讲一个大而全的支付系统——一个支付系统应提供支付渠道管理、支付网关、基本支付/退款/转账能力、支付记录/明细,及其相关的监控运维系统。至于所谓的账务清算,对账功能,账户体系,风控体系,现金流量管理,应纳入到「财务系统」,也应该是大佬们谈论的都是广义的「支付系统」吧!而本文只谈狭义的「支付系统」。

目前,支付的流程包含了三大部分:发起支付,发起退款,接收回调。考虑到吞吐量的影响,将原先同步的编程方式改为异步的编程方式,不出意外的话,将会使用到Java8的ExecutorService和CompletableFuture。

此外,还用到了公司其他的现成的东西:RabbitMQ、Redis、MongoDB。

笔者打算将这套支付系统设计成与具体业务无关,并纳入到公司的公共平台系统中。

一、发起支付

这部分主要讲客户端和服务端如何配合完成一次支付请求。服务端必须要有一个意识,最终发起支付的还是客户端,服务端提供一些必要的参数配置信息。

发起支付的架构图如下所示:

这里写图片描述

发起支付架构图

跟着标注的序号,可以跟踪到一个支付请求是如何发起的(Sequence Diagram就免了),流程描述如下:

  1. Submit a pay task,当客户端需要发起支付的时候,起始是向支付任务队列里面加入了一个新的支付任务,这个过程是异步实现的。先根据客户端提交的参数,构造好一个新的支付任务;
  2. Offer a task,开启一个异步任务,做的事情就是向MQ中添加一个新的支付任务,等待被消费;
  3. Pay task description,一旦异步任务被成功创建,将会把第一步构造好的支付任务信息直接return给客户端;
  4. Poll a task,与此同时,支付任务的消费者将新的支付任务poll下来进行执行;
  5. Send a pay request,这一步需要根据实际情况而定。并不是所有的支付请求都要先经过第三方支付平台,比如支付宝;而对于微信,则还需要凭支付参数申请一个prepay_id,再经由客户端发起支付;
  6. Response,没什么好说的,第三方渠道返回的支付必要参数;
  7. Cache result,至此,一个支付任务可以算是完成了,可以将任务的执行结果(无论成功与否)缓存在Redis中,随时等待客户端的回访;
  8. Query result,客户端在提交支付任务后,间隔一定时间后(建议2~3s),发起一个结果查询的请求;
  9. Query,直接进Redis查找结果;
  10. Synchronize,这是一个异步的操作,将支付任务的执行结果“顺便”同步到MongoDB中,并删除Redis中缓存的任务执行结果。持久化到MongoDB主要是为后续的容错,重试,数据分析等提供落地的数据源;
  11. Return,由Redis返回给应用服务器;
  12. Return payment,应用服务器再将最终的支付对象返回给客户端。

让我们更深入一点,我们来看三张Class Diagram:

① 先说说支付任务(PayTask)部分。PayTask和Payment两个都是MongoDB中的Document对象,但在任务执行期间,PayTask是用Redis进行缓存的,方便客户端随时发起Query,任务执行成功后,会生成Payment对象,最终PayTask和Payment都会持久化到MongoDB中。在PayService中,有对支付任务的一些基本操作,包括任务提交,取消,重试,构建等等。

这里写图片描述

② 再说说任务的执行(runner)。这部分和RabbitMQ紧密相关,一旦一个支付任务形成了,就会放入任务执行队列中,由消费者取出执行。在TaskRunner中,有两个基本的接口方法:run(task)、retry(task),分别是执行任务和重试任务。在AbstractPayTaskRunner中已经封装好了这两个方法,继承AbstractPayTaskRunner需要实现doTask方法,从返回值可以看出,这个过程是异步化的。关于Retry机制,用户可以设置重试与否,一旦设置了TaskInfo.needRetry=true(不出意外,默认就是允许重试),就启用了Retry机制。还可以设置重试的次数(TaskInfo.retryTimes),默认三次,分别间隔1s,2s,3s,间隔时间以公差为1的等差数列组成。当然不会让用户无限重试,系统内置有一个最大重试次数,最大重试次数内置为5次。

为什么是5次?

你感受一下,1s,2s,3s,4s,5s,整个请求链条就被拉长到了15s,这对客户端简直就是灾难了!!

这里写图片描述

③ 接着说一下支付渠道(PayChannel)。这部分设计与具体的支付渠道对接联系比较紧密了,包括支付参数配置,支付参数处理,签名/验签等等。

这里写图片描述

④ 最后解释一下支付参数(PayParams)。

这里写图片描述

大部分还是能看懂的,我解释几个关键的property:

1) appId,这是为了区分不同的产品所设置的。现实中,很有可能一个产品会申请与之对应的支付渠道,然后在支付平台中创建应用,设置好对应的支付参数,系统将会分配一个appId,凭此值就可以直接定位到各个支付参数。如果想再更完善一点,可以再区分一下测试环境和正式环境;

2) amount,这里代表的是支付金额的意思,但是这套支付系统的金额单位统一设置成 人民币【分】;

3) metadata,理论上,元数据这个字段没啥限制,要是非要说有限制,那么就是字段长度了——5000个字符。这个字段的想象空间还是很大的:用于填写丰富的交易相关信息,用于在增长智能系统产品中进行深入商业分析。包括交易行为多维分析、人群分析、产品转化路径、个性化推荐、智能补贴、定向推送等。看产品经理要怎么玩了;

5) credential,这个字段非常非常重要,其中装载的就是客户端最终发起支付请求的凭证,会作为Payment对象的一部分返回给客户端;

MongoDB的document字段设计

解释一下为什么要用MongoDB:

个人觉得,如果这个通用服务要得到较好的推广(甚至是开源),用MySQL等关系型数据库是不二之选,因为一个完整实用的系统,必然是少不了数据库的,如果一旦用了一些非传统的东西,必然会提高一部分人的对接成本。有的人一看不符合团队的技术栈,直接就不考虑了。

为什么我还是要用MongoDB呢?

① 团队的技术栈里面有这么个东西,不用白不用;
② MongoDB普及程度实在是不要太高,还不用上点NoSQL的东西,感觉自己分分钟被OUT掉了;
③ 要存储的数据结构需要支持动态扩展的特性,我就看中MongoDB的灵活性,如下是要存储的数据结构:

document_name = “Payment”

{
    "payId": "pay_Oyvrf9e9S1",
    "method": "yoogurt.taxi.pay",
    "version": "v1.0",
    "timestamp": 1473044885,
    "created": 1473042835,
    "paid": false,
    "appId": "app_iPGa98ab9ev",
    "channel": "wx",
    "orderNo": "20161899798416",
    "clientIp": "192.168.18.189",
    "amount": 10000,
    "subject": "充值订单-¥100.0",
    "body": "充值订单-¥100.0",
    "paidTime": null,
    "transactionNo": "",
    "metadata": {
        "user_id": "170204469176",
        "phone_number": "13811234567"
    },
    "credential": {
        "appId": "wx4932d1311e",
        "partnerId": "1269774001",
        "prepayId": "wx2016099",
        "nonceStr": "1e99d8fe92ba",
        "timeStamp": "1473042837",
        "packageValue": "Sign=WXPay",
        "sign": "1CECCEDEBE"
    },
    "extra": {},
    "statusCode": "",
    "message": "",
    "description": ""}

其中,metadata,credential,extra这类字段,并没有一个特别固定的规范,用MySQL要冗余一下字段才行,或者针对每个渠道去分表,想想都觉得烦!

MySQL

因为这套支付系统被设计成为支持多应用,多渠道,所以此处用到MySQL存放一些应用配置。 E-R图免了,直接上数据库表结构:

① pay_channel:可供接入的支付渠道

这里写图片描述

② app_settings:支付应用信息

这里写图片描述

③ app_channel:应用已接入的支付渠道

这里写图片描述

④ alipay_settings:支付宝参数设置

这里写图片描述

⑤ wx_settings:微信app支付参数设置

这里写图片描述

如果想要增加支付渠道,只需要添加一张对应的支付参数设置表。

二、发起退款

不出意外,客户在平台的每笔订单都可以发起退款,而且还能分批退,也就是同一个订单,可以多次发起退款申请,只要保证退款总额不超出实付总额。 架构图如下所示:

这里写图片描述

发起退款架构图

跟发起支付请求的流程有很多相似之处,不再一一解释了,两个关键的地方说明一下:

客户端发起退款请求的时候,需要携带payId,就是支付对象的id。这就意味着,支付系统的调用方需要维护payId与orderNo的对应关系,务必在客户端发起退款请求之前,获取到正确的payId;
承接上一步,这才有了图中的第5、6个步骤,从MongoDB中查询之前的支付对象。第三方渠道通常会要求在退款的时候指定一个退款单号,因为一笔订单可以分多次退款,所以不建议将订单号作为退款单号使用。这里的退款单号由支付系统生成并维护。
这部分的执行流程和之前类似,客户端发起退款请求,形成一个退款任务(RefundTask),放入任务队列中,消费者取出并执行各自的业务逻辑,退款成功会生成Refund对象,并持久化到MongoDB中。

MongoDB

document_name = "Refund"

{
    "payId": "pay_vfvS0m1",
    "method": "yoogurt.taxi.pay",
    "version": "v1.0",
    "timestamp": 1473044885,
    "created": 1473042835,
    "refundId": "refund_kmrf9wSr1em",
    "appId": "app_iGa8abLe9ev",
    "orderNo": "20161899798416",
    "clientIp": "192.168.18.189",
    "amount": 10000,
    "succeedTime": 1473150835,
    "transactionNo": "64059968740554",
    "refundStatus": "success",
    "message": "",
    "metadata": {
        "user_id": "170204469176",
        "phone_number": "13811234567"
    },
    "description": ""} 

三、接收回调

这部分功能被设计成了事件驱动类型,所以webhooks当仁不让。

因为各个渠道的回调内容都不尽相同,所以这部分设计会按支付渠道切分。

架构图如下:

这里写图片描述

处理回调事件

用户在支付完毕后,第三方支付渠道通过发起支付时指定的回调地址对商户进行支付成功的异步通知。

这部分的执行流程和之前类似,在各自的PayChannel中解析好回调参数,形成一个回调事件(Event),并持久化到MongoDB中,然后再生成一个回调任务(EventTask),放入任务队列中,消费者取出并执行各自的业务逻辑,这里的消费者就是上游的业务服务系统。

MongoDB

document_name = “Event”

{
    "eventId": "evt_la06Co7wq",
    "created": 1427555016,
    "eventType": "pay.succeeded",
    "data": {
        "payId": "pay_OvP88CSm1",
        "method": "yoogurt.taxi.pay",
        "version": "v1.0",
        "timestamp": 1473044885,
        "created": 1473042835,
        "paid": false,
        "appId": "app_iGa9aLe9ev",
        "channel": "wx",
        "orderNo": "20161899798416",
        "clientIp": "192.168.18.189",
        "amount": 10000,
        "subject": "用户充值-¥100.0",
        "body": "充值订单-¥100.0",
        "paidTime": null,
        "transactionNo": "",
        "statusCode": "",
        "message": "",
        "metadata": {
            "user_id": "170204469176",
            "phone_number": "13811234567"
        },
        "credential": {
            "appId": "wx4932b511e",
            "partnerId": "1269774001",
            "prepayId": "wx201609051039",
            "nonceStr": "1e9d8fddad",
            "timeStamp": "1473042837",
            "packageValue": "Sign=WXPay",
            "sign": "1C0K3C95AKB"
        },
        "extra": {

        },
        "description": ""
    },
    "retryTimes": 0} 

特别说明一下data字段:

如果是支付成功事件,则返回对应的Payment对象;

如果是退款成功时间,则返回对应的Refund对象。


1月13日,SDCC 2017之数据库线上峰会即将强势来袭,秉承干货实料(案例)的内容原则,邀请了来自阿里巴巴腾讯微博网易等多家企业的数据库专家及高校研究学者,围绕Oracle、MySQL、PostgreSQL、Redis等热点数据库技术展开,从核心技术的深挖到高可用实践的剖析,打造精华压缩式分享,举一反三,思辨互搏,报名及更多详情可点击此处查看。

版权声明:本文为博主原创文章,未经博主允许不得转载。

支付系统整体设计:整体架构设计以及注意要点(一)

016-11-23 01:43:00  来源: 凤凰牌老熊 导读: 在支付系统中,支付网关和支付渠道的对接是最核心的功能。其中支付网关是对外提供服务的接口,所有需要渠道支持的资...
  • sz_bb
  • sz_bb
  • 2016年11月23日 09:34
  • 3072

支付系统高可用架构设计实战

对于互联网应用和企业大型应用而言,多数都尽可能地要求做到7*24小时不间断运行,而要做到完全的不间断运行可以说“难于上青天”。...
  • softwave
  • softwave
  • 2016年08月13日 17:44
  • 4371

支付系统整体设计:整体架构设计以及注意要点(二)

支付网关前置   支付网关前置是对接业务系统,为其提供支付服务的模块。它是所有支付服务接口的集成前置,将不同支付渠道提供的接口通过统一的方式呈现给业务方。这样接入方就只需要对接支付网关,增加和调...
  • sz_bb
  • sz_bb
  • 2016年11月23日 09:38
  • 987

【备忘】Dubbo与Zookeeper视频教程分布式架构+第三方支付

第一部分:基础篇视频教程 第二部分:高级篇视频教程 第三部分:高可用架构篇视频教程 课程大纲: 1 课程介绍 20分钟 2 使用Dubbo对传统工程进行服务化改造的思路介绍 15分钟...
  • qq_38473938
  • qq_38473938
  • 2017年08月05日 11:13
  • 213

第三方支付架构设计之—自有账户支付

笔者在上一篇blog>中已经稍微全面的阐述了第三方支付架构设计中的账户体系,在该体系中,其实涉及了各种各样的账户:银行侧账户(包括用户在银行侧的账户:用户借记卡,信用卡,商户在银行侧的清算账户,结算账...
  • tenfyguo
  • tenfyguo
  • 2015年01月31日 23:22
  • 17352

一个典型PHP支付系统的设计与实现

由于公司业务需要,花两周时间实现了一个小型的支付系统,麻雀虽小五脏俱全,各种必须的模块如账户加锁,事务性保证,流水对帐等都是有完整实现的,整个开发过程中有很多经验积累,再加上在网上搜索了一下,大部分都...
  • JF_2012
  • JF_2012
  • 2015年12月28日 13:48
  • 1632

第三方支付架构设计之—帐户体系

第三方支付架构设计之—帐户体系 一,      什么是第三方支付?         什么是第三方支付?相信很多人对这个名字很熟悉,不管是从各种媒体等都经常听到,可以说是耳熟能熟。但,如果非得给这个...
  • tenfyguo
  • tenfyguo
  • 2014年12月01日 23:29
  • 75938

支付系统开发--简易版支付系统介绍

一、工程结构 pay-common-parent      项目的Maven父配置工程 pay-common             公共工程,所有项目均可引用 pay-common-config  ...
  • whs_321
  • whs_321
  • 2016年06月17日 16:36
  • 3096

分布式架构学习之:013--基于Dubbo的分布式系统架构介绍(以第三方支付系统架构为例)

基于Dubbo的分布式应用架构规划(以第三方支付系统为例) 结合业务场景,对系统的应用类型进行划分: (1) 服务子系统 ------ 账户、交易、对账、结算、打款、风控…… (2) 内部管理应用 -...
  • xxssyyyyssxx
  • xxssyyyyssxx
  • 2017年05月11日 00:08
  • 1176

互联网支付系统整体架构详解

博主说:通过阅读本文,可以帮助大家在宏观层面上,对互联网支付系统的整体架构有一个更好的认识。 正文  从产品分类、模块功能和业务流程,了解支付产品服务的设计。  支付产品模块是按照支付场景来为业务方提...
  • qq_35246620
  • qq_35246620
  • 2017年05月17日 13:21
  • 5078
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:简易支付系统的架构设计
举报原因:
原因补充:

(最多只允许输入30个字)