886 lines
26 KiB
Markdown
886 lines
26 KiB
Markdown
- 金融牌照
|
||
- 平台跳转
|
||
- 商家-收款方
|
||
- 用户-持卡人
|
||
- 中间-卡组织
|
||
- Visa
|
||
- MasterCard
|
||
- Card Network
|
||
- 中间转发各种各样的支付请求
|
||
- 无需额外对接
|
||
- 用户有各种各样的银行卡
|
||
- 商家有作为收单方也有不同银行帐号
|
||
- 减少对接以及合规的负担
|
||
- Payment Service Plantform(PSP)
|
||
- 提供一个简单的API
|
||
- 调用API,PSP处理这些支付请求
|
||
- 收取手续费0.5%-1%
|
||
- 支付系统
|
||
- 记账+内部清算
|
||
- 用来记录支付事实
|
||
- 支付流程
|
||
- 商家发起一个支付请求
|
||
logseq.order-list-type:: number
|
||
- 调用外部PSP的API,执行支付的请求
|
||
logseq.order-list-type:: number
|
||
- PSP执行完成,记录这笔账
|
||
logseq.order-list-type:: number
|
||
- 通知对应的用户支付结果
|
||
logseq.order-list-type:: number
|
||
- 每天定时对账目进行清算
|
||
logseq.order-list-type:: number
|
||
- Function Requirement
|
||
- merchant send payment request
|
||
- client can pay
|
||
- merchant can query payment status
|
||
- NonFunction Requirement
|
||
- Sercutiy
|
||
- Reliability-handle failure
|
||
- consistency
|
||
- correctness
|
||
- scaleability-PSP API
|
||
- 
|
||
- Initiation
|
||
logseq.order-list-type:: number
|
||
- Execution
|
||
logseq.order-list-type:: number
|
||
- Recording
|
||
logseq.order-list-type:: number
|
||
- Notification
|
||
logseq.order-list-type:: number
|
||
- Clearing
|
||
logseq.order-list-type:: number
|
||
- Payment integration
|
||
- serve-to-serve/PCI审计
|
||
logseq.order-list-type:: number
|
||
- SDK,如果崩溃就会无法付款,存在供应链攻击的分享
|
||
logseq.order-list-type:: number
|
||
- 隔离用户卡号
|
||
- iframe
|
||
logseq.order-list-type:: number
|
||
- hosted redirect
|
||
logseq.order-list-type:: number
|
||
- 跳转到第三方的页面
|
||
- data model
|
||
- single center table
|
||
logseq.order-list-type:: number
|
||
- 审计要求
|
||
- 不能够修改
|
||
- three table: order+payment+ledger
|
||
logseq.order-list-type:: number
|
||
- payment {
|
||
charge_id
|
||
order_id
|
||
payment_method
|
||
PSP
|
||
status{created, start, processing, successed, failed}
|
||
amount
|
||
currency
|
||
created_at
|
||
updated_at
|
||
idem_unique_key
|
||
}
|
||
- retry
|
||
- timeout
|
||
logseq.order-list-type:: number
|
||
- exponential backoff
|
||
logseq.order-list-type:: number
|
||
- jitter(+-15%)控制请求分布更加均匀
|
||
logseq.order-list-type:: number
|
||
- DLQ(exhaust)
|
||
logseq.order-list-type:: number
|
||
- PSP调用callback URL
|
||
- 失败后,service主动轮询PSP
|
||
- 采用复式记帐
|
||
- 写入到append-only数据库
|
||
- AMAZON QLDB(Quantum Ledger Database)
|
||
- Alibaba ledgerDB
|
||
- 专门为金融优化的数据库
|
||
- 对账
|
||
- 银行发送settlement file
|
||
- Turning the database inside-out
|
||
-
|
||
- Text
|
||
- 今天我们来讲一个支付系统
|
||
这种系统你还是需要一点生活经验的
|
||
因为它不是真的让你去划账
|
||
你又没有这个金融牌照
|
||
拿什么去搞这笔钱
|
||
你有可能上网去买个衣服
|
||
还要跳转到另一个平台去付款
|
||
那比如说用户拿他的信用卡去买东西
|
||
那用户这边就是持卡人
|
||
商家里面叫做收款方
|
||
两边对应着各自有一个银行
|
||
像用户这边就是发卡行
|
||
然后对面就是收单行
|
||
中间其实还有一个叫做卡组织的东西
|
||
比如说Visa或者MasterCard
|
||
他们都是card network
|
||
然后中间会转发各种各样的支付请求
|
||
用户可能会有各种各样的银行卡
|
||
所以会对应不同的发卡行
|
||
然后商家作为收单方也会有不同的银行账号
|
||
那你要是挨个去对接呢
|
||
在工作量肯定是吓死人
|
||
合规的负担也会比较重
|
||
而且银行里面的技术更新通常会比较慢
|
||
有时候你可能看到一些上古的技术栈
|
||
所以通常我们会去找一个叫做PSP的平台
|
||
就是payment service platform
|
||
通常这个PSP平台呢
|
||
会给我们一个比较简单的API
|
||
然后我们直接去调用这个API
|
||
然后由PSP来处理这些支付请求
|
||
当然它会向我们收取一定的手续费
|
||
通常是可能是0.5%到1%这个区间
|
||
那话说回来了
|
||
如果有人能去处理这个支付
|
||
那我们还设计什么支付系统呢
|
||
实际上这个支付系统想让你设计的是一个记账
|
||
再加上内部清算的这么一个流程
|
||
也就是说它是一个用来记录支付事实的系统
|
||
那这样这个问题就会简单很多了
|
||
我们可以拆分一下用户支付的这个流程
|
||
首先就是商家里面会发起一个支付请求
|
||
然后我们应该去调用这个外部的PSP的API
|
||
去执行这个支付的请求
|
||
然后PSP执行完之后
|
||
我们应该去进行一下记录
|
||
就是说我们会把这笔账记下来
|
||
所以记下来之后不管它的成功和失败
|
||
你都应该去通知一下对应的用户
|
||
最后可能是每天对这个账目进行一下清算
|
||
其实真正的过程应该比这个要复杂一点
|
||
不过我觉得能够覆盖到这么几个步骤
|
||
应该已经差不多了
|
||
毕竟你的时间也有限
|
||
所以我先把这个放在这
|
||
我们来看一下它的功能性需求应该是什么
|
||
功能性需求其实还是比较明确的
|
||
首先商家这边
|
||
他要能发起一个支付请求
|
||
然后客户这边
|
||
要能去付费
|
||
然后商家这边应该能去查看这个订单的支付状态
|
||
非功能性需求呢
|
||
首先肯定是安全吧
|
||
这是肯定不用说的
|
||
所有和资金有关的东西
|
||
security都是第一步的
|
||
然后应该是可靠性
|
||
他要能自己去handle一些failure之类的
|
||
接下来还有一个consistency
|
||
因为肯定嘛
|
||
我们希望一笔钱只被扣款一次
|
||
然后这笔账被记下来
|
||
所以一致性很重要
|
||
另外一个比较重要的
|
||
常见的金融相关是正确性
|
||
肯定是希望你的账永远是对的
|
||
这里有一个不太确定的地方
|
||
就是说这个scalability
|
||
我们会不会去考虑
|
||
因为实际上你是在调用一个外部的API
|
||
所以那么很大程度上
|
||
你的bottleneck其实不在你自己这里
|
||
在外部
|
||
这种情况下
|
||
我建议我们就一开始不要先去考虑scalability了
|
||
后面如果有条件的话
|
||
我们再研究
|
||
搞清楚这些之后
|
||
我们就可以去画一个high level的diagram
|
||
OK
|
||
这个图还是很简单的
|
||
只不过画的比较简略
|
||
我们要来挨个展开一下
|
||
那首先第一件事就是用户要去付款
|
||
那应该做什么
|
||
首先从商家这一方来说
|
||
我的服务器上肯定要给你提供付款这么一个逻辑
|
||
那也就是说我们应该做就是支付集成
|
||
支付集成
|
||
支付集成其实还是有不少方案的
|
||
比如说最简单就是说
|
||
我可以做一个server to server
|
||
对吧
|
||
商家从用户这边去收集用户的这个卡号和他的这个CVV密码
|
||
然后后端把这个收集他的卡号和密码直接发给银行或者是卡网络
|
||
那这样其实你就相当于自己建立一个PSP了
|
||
对吧
|
||
因为你需要去取得支付机构的牌照
|
||
一般大家也不会这么去做
|
||
因为商家会接触到这些 PAN (primary account number)
|
||
这些乱七八糟的东西
|
||
那也就是说你需要通过这个PCI的审计
|
||
审计这个事情通常比较复杂
|
||
合规门槛高
|
||
而且也比较贵
|
||
除了技术的复杂度之外呢
|
||
初期的投入也很高
|
||
基本上只有大型的企业会用这种方案了
|
||
但是好处也会比较明显
|
||
就是说一旦通过了这个PCI审计
|
||
自主权就相对比较大了
|
||
不再需要去依赖第三方的支付平台
|
||
可以省下一笔第三方的通道费
|
||
自己也可以去控制一些逻辑
|
||
比如说增加一些错误处理啊
|
||
分期 分账啊 这些玩意儿
|
||
那另外一个比较简单的一点方案
|
||
就是说我引入一个支付平台的SDK
|
||
这个应该是我们比较常见一种方案
|
||
大家通常用着stripe paypal
|
||
或者说国内有微信 支付宝
|
||
其实都提供这种方案
|
||
这样一来PSP提供一个js library
|
||
商家把它呢
|
||
嵌入到自己的网站上
|
||
自己去开发一些定制的UI
|
||
付费的时候
|
||
这个SDK把用户的卡号
|
||
直接传到这个PSP去
|
||
然后会返回一个token
|
||
商家用这个token来调用PSP的API
|
||
但是本身不接触用户的卡号
|
||
这样一来就可以避免
|
||
去接触用户的账号信息
|
||
但是这样说
|
||
你的瓶颈就在这个SDK上
|
||
如果这个SDK它崩溃了
|
||
那用户就不能付款
|
||
理论上这种
|
||
JavaScript SDK是通过fetch()
|
||
然后送到PSP的domain去的
|
||
不需要经过商家的服务器
|
||
但是脚本实际上
|
||
还是跑在商家页面里
|
||
所以这里如果你用XSS注入(Cross-Site Scripting跨站脚本)
|
||
或者是MageCart植入
|
||
都可以在用户的浏览器里面
|
||
去窃取到这个输入的卡号
|
||
那也就是说
|
||
这里其实存在供应链攻击的风险
|
||
那商家这一端
|
||
就应该要保护自己的前端页面
|
||
做上一些安全策略
|
||
比如说去做一个
|
||
CSP(内容安全策略 Content Security Policy) 或者是SRI (子资源完整性 Sub-resource Integrity)这些东西
|
||
只允许去加载可信的脚本
|
||
并且你要对这个脚本
|
||
验证一下哈希值
|
||
再往后退一步的话
|
||
你可以使用一个iframe
|
||
iframe相当于一个
|
||
更加安全的Sandbox管理
|
||
隔离敏感数据
|
||
也减小了攻击面
|
||
商家只得到支付的token
|
||
那么现在的SDK
|
||
实际上都会采用混合模式
|
||
SDK直接在底层
|
||
注入一个iframe
|
||
浏览器端会使用
|
||
iframe hosted field
|
||
对商家隐藏银行卡的信息
|
||
然后使用PSP的公钥
|
||
去做CSE(客户端加密 client-side encryption)
|
||
把这个卡号加密起来
|
||
最后Post请求发到PSP去
|
||
PSP拿到这个密文之后
|
||
会进行解密
|
||
然后返回一个不可逆的token
|
||
商家还是拿这个token去做扣款
|
||
不再去接触这个卡号
|
||
但是iframe的问题在于
|
||
前端的CSS和响应式
|
||
会设到这个iframe的限制
|
||
这会对你的UI的开发
|
||
产生一定的限制
|
||
有能力企业还是会选择
|
||
基于JavaScript SDK
|
||
自己去做二次开发
|
||
这样来说自由度高一点
|
||
但是同样成本也会更加高一点
|
||
最后一种
|
||
就会进行hosted redirect
|
||
直接去走一个托管的支付页面
|
||
用户点击下单
|
||
然后一个302
|
||
跳转到PayPal或者Strape
|
||
这样的平台上去
|
||
等你用户付款完了之后
|
||
再重定向回来
|
||
这其实是最简单的对吧
|
||
PCI的负担最低
|
||
运维也最简单
|
||
一旦出了故障
|
||
就全靠PSP自己去兜底去了
|
||
同样它的自由度也是最低的
|
||
因为你的UI是人家的
|
||
品牌也是人家的
|
||
全看PSP
|
||
而且这里会多一步跳转
|
||
这代表着
|
||
这里的用户的转化率
|
||
会有些损失
|
||
总的来说呢
|
||
你的可定制化越高
|
||
成本越高
|
||
不仅仅是技术实现的成本
|
||
还会有PCI的责任
|
||
和安全管控的成本
|
||
要求又会更多一点
|
||
等你集成了支付系统之后
|
||
这里就会出现第二个问题
|
||
我们花了这么大劲 对整个支付的流程做了下隔离
|
||
那既然隔离了
|
||
商家要怎么来记录这笔交易呢
|
||
其实我们只是隔离了
|
||
商家和用户的银行卡信息
|
||
用户这边提交完付款信息之后
|
||
PSP还是会把这个token发回来
|
||
这个token也叫nonce
|
||
后续的扣款
|
||
还是要商家拿着这个nonce
|
||
去和PSP发请求的
|
||
nonce一般来说是一次性的
|
||
有的时候你会看见
|
||
商家问你
|
||
需不需要保存这个支付信息
|
||
如果你勾取了这条
|
||
商家会再请求一次
|
||
换取一个可以复用的token
|
||
这里我们简单一点
|
||
就只说这个单次的nonce
|
||
有了这些信息之后
|
||
其实你已经可以开始
|
||
去写数据库了
|
||
那么这里就会有一个问题
|
||
我这数据库要怎么存
|
||
第一个想法通常是
|
||
我能不能用一张表
|
||
存所有的信息
|
||
对吧
|
||
我每次有一笔支付请求
|
||
我就记下来
|
||
用户买了什么东西
|
||
花了多少钱
|
||
然后这个支付的状态是什么
|
||
它的卡是什么
|
||
这样每一条交易
|
||
对应着这个表里的一条记录
|
||
但这里可能会有一个问题
|
||
因为支付这个东西
|
||
它是受到合规限制的
|
||
任何和金融相关的东西
|
||
都需要经过审计
|
||
支付数据在这里
|
||
应该是不可更改的
|
||
你每次有什么操作
|
||
不能说去覆盖
|
||
之前的这个状态
|
||
所以最好来说
|
||
还是要去拆分一下数据表
|
||
这个里面
|
||
应该会有三种不同的状态
|
||
首先是你的业务状态
|
||
这里有哪些订单
|
||
用户这个订单
|
||
对应着什么商品
|
||
第二个是你的交易状态
|
||
用户发起了这个
|
||
支付请求之后
|
||
有没有付钱?
|
||
用什么卡付钱?
|
||
付了多少?
|
||
然后现在是被拒绝了
|
||
还是已经支付成功了
|
||
第三个状态
|
||
就是你的资金状态
|
||
这笔钱有没有到账
|
||
这笔账要不要进行分配
|
||
有多少是最后要给商家的
|
||
有多少是最后
|
||
要付给支付渠道的手续费
|
||
所以这里三个状态
|
||
如果要做责任分离的话
|
||
至少是三张数据表
|
||
这三张数据表的读写模式
|
||
应该是完全不一样的
|
||
比如说这个资金状态这张表
|
||
它就应该是不可更改的
|
||
对于支付系统
|
||
这种合规驱动业务来说
|
||
有条件
|
||
我们还是多存一张表
|
||
也好过在同一张表里
|
||
改来改去
|
||
理论上我们可能是需要
|
||
order table
|
||
加上一个payment table
|
||
再加上一个ledger的账簿
|
||
这题的核心还是payment
|
||
那我们还是关注一下
|
||
payment表的数据结构
|
||
假设这里
|
||
就是这个payment表
|
||
我们可能需要哪些字段
|
||
首先你肯定会要有
|
||
这个charge ID
|
||
第二
|
||
然后你会有order ID
|
||
接下来可能是payment method
|
||
然后是对应的PSP是什么
|
||
这笔订单的status是什么
|
||
status可能是说
|
||
我一开始先去创建
|
||
这张支付的请求
|
||
然后这个支付开始处理
|
||
最后可能是success
|
||
或者是failed
|
||
这里通常是需要一个
|
||
状态机来表示
|
||
然后这里面会有多少钱
|
||
它对应的货币是什么
|
||
这笔支付是什么时候创建
|
||
又是什么时候更新的
|
||
最后这里可能需要一个
|
||
unique ID来做这个幂等
|
||
我直接叫他
|
||
idempotency key了
|
||
这张表要怎么存呢
|
||
通常来说和金融相关
|
||
我们都要注意一致性
|
||
但是一致性
|
||
也分内部一致性
|
||
外部一致性
|
||
内部一致性其实好处理
|
||
对吧
|
||
我直接拿关系性数据库
|
||
这关系性数据库
|
||
是有一致性保障的
|
||
那么外部一致性
|
||
这个时候我们就要盘一下
|
||
这个workflow
|
||
用户在这里去进行下单
|
||
他打开这个客户端
|
||
点击一个checkout
|
||
然后开始支付
|
||
用户的预期是
|
||
我就把这一单结了
|
||
这里其实最怕
|
||
发生的是一个
|
||
double charging
|
||
或者double spending
|
||
比如说用户
|
||
网卡了一下
|
||
然后连着点了好几次
|
||
然后接下来服务器这边
|
||
就收到多个请求
|
||
那服务器这边
|
||
多个请求
|
||
肯定不能通通去扣款
|
||
对吧
|
||
他肯定要做一个
|
||
exactly once
|
||
这个exactly once
|
||
就需要依赖了
|
||
这个idempotency key了
|
||
当用户点开
|
||
这个checkout页面的时候
|
||
服务器这个时候
|
||
应该会生成一个幂等键
|
||
然后后续流程
|
||
全都带着这个幂等键
|
||
用户点开这个页面
|
||
服务器把这幂等键
|
||
发给客户
|
||
客户下完单之后
|
||
带这个幂等键
|
||
再返回到payment service这边
|
||
service这端
|
||
我看到这个idempotency key
|
||
开始去做de-duplication
|
||
拦截重复的请求
|
||
这样就算用户点了多次
|
||
也只有一个请求
|
||
会被进行后续的处理
|
||
处理的时候
|
||
需要去更新
|
||
payment表的status状态
|
||
比如说一开始是created
|
||
然后传给他一个idempotency key
|
||
然后当你收到一个
|
||
合法的请求之后
|
||
就转入这个start状态
|
||
表示这个支付请求已经收到了
|
||
下一步就要去调用
|
||
外部平台的这个支付的API了
|
||
也就是说调用这个PSP
|
||
PSP收到这个请求开始处理
|
||
就中间可能会有一些风控啊
|
||
或者是授权之类的
|
||
交易授权这里可快可慢
|
||
通常你这里提交完
|
||
至少应该收到一个明确的信号
|
||
比如说“你的支付请求已经受理”
|
||
或者告诉你“交易失败了”
|
||
然后这个时候payment表里的status
|
||
应该去转入到processing阶段
|
||
这里最好画一下这个状态机
|
||
好 有了这个状态机之后
|
||
我就可以把这个简化一下
|
||
由于PSP是异步处理
|
||
所以我们实际上不知道PSP
|
||
什么时候能处理好
|
||
但通常来说PSP应该先给你一个
|
||
“我已经在处理了”
|
||
那如果PSP就没有给你一个信号
|
||
比如说有网络抖动
|
||
对吧
|
||
他给你信号
|
||
但是你没有收到
|
||
所以这里你应该引入一个retry的机制
|
||
那通常来说
|
||
一旦我们要加入retry的话
|
||
那你就要想到retry的四个要素了
|
||
第一个就是timeout
|
||
这个retry的等待时间
|
||
应该是有个最大限制
|
||
超过这个timeout的时间之后
|
||
你才去触发这个retry
|
||
那第二个就是backoff
|
||
我是间隔一秒钟
|
||
还是间隔两秒钟
|
||
还是说我不断去增加这个时间间隔
|
||
通常来说
|
||
这里比较推荐的是
|
||
做一个exponential backoff
|
||
我第一次等待一秒
|
||
然后第二次等待两秒
|
||
然后第三次等待四秒
|
||
这下来等待八秒
|
||
每次延长这个间隔的时间
|
||
这样不会给后端造成太大压力
|
||
那第三点是做一个jitter
|
||
jitter的意思是
|
||
我每次向后端发请求
|
||
我加入一些网络抖动
|
||
比如说我加一个正负15%
|
||
比如说我按照某个频率
|
||
给后端去发送这个重试的请求
|
||
但是这里有另一个服务器
|
||
它也采用这个频率
|
||
跟你同步的发送这个请求
|
||
那这个时候对后端来说
|
||
它可能在同一个时间
|
||
会收到多个请求同时涌入
|
||
然后在下一个间隔
|
||
大家又同时暂停了
|
||
然后再到下一个间隔
|
||
又同时涌入
|
||
加入jitter之后
|
||
我可以对这个间隔时间
|
||
做一个正负15%的抖动
|
||
这样可以将这个分布
|
||
打得更加均匀一点
|
||
第四个要素就是
|
||
dead letter queue了
|
||
所谓死信队列
|
||
retry这里
|
||
我们最好设置一个
|
||
最大的重试次数
|
||
一旦超过这个次数之后
|
||
你可以当做
|
||
你的后端这个PSP
|
||
已经宕机了
|
||
你就不要再进行retry
|
||
你应该把这个消息
|
||
放入这个死信队列
|
||
然后去进行一些处理
|
||
比如说你本地
|
||
做一个整体的回滚
|
||
通常可能还要再
|
||
熔断一段时间
|
||
那么对于外部的PSP来说
|
||
因为你这里不断的在重视
|
||
所以外部的PSP
|
||
也要去做
|
||
这个de-duplication的操作
|
||
那么这样一来
|
||
它代表着你这个幂等键
|
||
也应该同步的
|
||
穿透到外面的PSP去
|
||
这样至少也能
|
||
增加一道保险
|
||
等PSP返回消息
|
||
提交成功之后
|
||
就可以先给用户
|
||
返回一下信息了
|
||
比如说你告诉用户
|
||
“已经下单”
|
||
“等待确认”
|
||
然后PSP里面
|
||
会进行一步的处理
|
||
通常来说
|
||
我们会给PSP
|
||
一个callback URL
|
||
PSP处理完这笔支付
|
||
会调用这个URL
|
||
通知我们
|
||
但是如果这个PSP
|
||
调用这个callback URL
|
||
失败了
|
||
它没通知到我们
|
||
对吧
|
||
网络还是有抖动
|
||
所以这个时候
|
||
你还要再补上一个轮询
|
||
超过一段时间
|
||
没有收到这个callback
|
||
那就是主动查询一下
|
||
当PSP处理完之后呢
|
||
这状态机
|
||
就应该进入这个success
|
||
或者是fail
|
||
得到一个固定结果
|
||
然后把这个结果通知用户
|
||
那如果这里进入success
|
||
就代表着扣款成功了
|
||
那这个时候
|
||
你应该就要去写这个ledger
|
||
这个里面又会涉及到
|
||
对账户 交易和分录的建模
|
||
所以也是一个比较复杂的系统
|
||
一般来说
|
||
行业的标准是
|
||
商务的实体要采取复式记账(double-entry bookkeeping)
|
||
也就是说
|
||
每笔交易至少要
|
||
影响到两个账号
|
||
产生一组
|
||
金额相等 方向相反的记录
|
||
这里非要扯一下的话
|
||
你应该考虑到ledger的金融属性
|
||
账本这种东西
|
||
一旦你写入之后
|
||
就是不可变的
|
||
所以这个ledger
|
||
通常应该写入到一个
|
||
append-only的数据库去
|
||
这里除了你能想到的
|
||
关系型数据库之外呢
|
||
通常来说
|
||
对于这种specific domain
|
||
你可以去猜一下
|
||
问一下面试官
|
||
是不是应该有一个
|
||
专用的技术栈
|
||
那ledger这里
|
||
确实是存在ledger使用的数据库
|
||
比如说amazon会有一个QLDB(Quantum Ledger Database)
|
||
或者是Alibaba
|
||
开源的ledgerDB这种东西
|
||
他们都提供了SQL接口
|
||
然后专门为金融事务
|
||
进行了优化
|
||
这种专业优化过的数据库
|
||
通常来说
|
||
开发难度会比较低
|
||
然后很多你需要的功能
|
||
都是已经内置好了
|
||
比如说审计啊
|
||
或者是加密之类的
|
||
最后一步就是进行对账了
|
||
因为任何在线链路
|
||
都可能遇到bug
|
||
所以银行这里
|
||
应该定时发过来
|
||
这个settlement file(结算报告)
|
||
我们可以每天执行一次
|
||
对账任务
|
||
如果有问题呢
|
||
那就可以触发一个自动补偿
|
||
或者是转到人工进行审核
|
||
这样一来
|
||
应该就是这个完整的支付流程
|
||
你看这个流程有问题吗
|
||
其实还是有的
|
||
你记得这里
|
||
我们补了一个轮询
|
||
有一笔交易一直在审核
|
||
然后商家这边
|
||
等不到这个callback
|
||
于是开始进行轮询
|
||
轮询几次
|
||
到达这个最大上限
|
||
然后写入了一个fail
|
||
fail了之后
|
||
PSP这边突然发现
|
||
交易过了
|
||
于是这个PSP
|
||
调用了这个callback
|
||
那这个时候
|
||
就应该写入一个成功
|
||
对吧
|
||
这个就叫做状态机的跳变
|
||
它也不是不能解决
|
||
这里我们可以使用一个
|
||
单调状态机
|
||
一旦你这个状态进入fail之后
|
||
就不能再覆盖成这个success
|
||
或者说我这里
|
||
打一个版本号的补定
|
||
每次进行更新的时候
|
||
我要去比较这个版本号
|
||
那最差的情况下
|
||
你还可以等到
|
||
这个日终的对账环节
|
||
总之你的账不能出错
|
||
但是这个问题的根源在于
|
||
你每次状态机的变化
|
||
实际上是对于这个
|
||
paymentDB的一条记录
|
||
发起了一次UPSERT
|
||
用UPSERT这个语句
|
||
覆盖之前的状态
|
||
但是这个问题也不大
|
||
因为大部分PSP
|
||
其实都是这条方案
|
||
一张表去维护
|
||
这个实时的操作状态
|
||
另一张账务表
|
||
会负责这个资金的守恒
|
||
这样一来
|
||
它开发成本也比较低
|
||
维护成本也低
|
||
方案本身呢
|
||
也比较成熟
|
||
也是合规的
|
||
但如果你要进一步呢
|
||
其实这里确实是存在
|
||
另一种方案
|
||
之前我们也讲过一些
|
||
数据库和流系统互换的案例
|
||
有过这么一篇
|
||
《Turning the database inside-out》
|
||
我们在这里去掉这个数据库
|
||
然后采用一个event store
|
||
event store呢
|
||
可以使用Kafka
|
||
或者是Apache Pulsar
|
||
之类的流系统
|
||
也有可能是一个
|
||
append-only的db
|
||
当然你也可以两个都使用
|
||
那这样一来
|
||
所有的变更
|
||
都作为一个事件
|
||
写入到这个event store里面
|
||
从这个event store
|
||
作为source of truth
|
||
在我们再通过
|
||
projection或者view的方式
|
||
来生成视图表
|
||
加速查询
|
||
事件一旦写入呢
|
||
就不再更改
|
||
任何的纠错
|
||
以新事件的方式写入
|
||
进行冲正
|
||
不少互联网公司
|
||
其实都会采用
|
||
这种ledger系统
|
||
比如说stripe用过
|
||
Coinbase用过
|
||
Square也用过
|
||
都是一个不可变的账本
|
||
再加上事件流的形式
|
||
这样一来
|
||
你既可以获得一个
|
||
金融系统的正确性
|
||
也可以获得一个
|
||
互联网系统的可拓展性
|
||
但它的代价也比较明显
|
||
这里有一个比较高的
|
||
系统复杂度
|
||
那开发和运维的成本
|
||
也比较高
|
||
团队本身呢
|
||
需要对流系统和
|
||
金融系统都比较熟悉
|
||
具体怎么选型
|
||
还是要看你的场景
|
||
一旦你接受了
|
||
这个流系统的这种设定
|
||
那其实这个系统
|
||
可以产生很多的变化
|
||
比如说
|
||
我将这个消息队列
|
||
放到前面去
|
||
进行一下削峰填谷
|
||
同时进行数据落盘
|
||
那这样一来的话
|
||
Payment Service
|
||
作为一个Broadcast
|
||
把你每次的事件
|
||
投放到不同的数据库去
|
||
别的结构可以不变
|
||
每次对账的时候
|
||
你可以在消息队列的前端
|
||
进行事件的重放
|
||
重新计算一下
|
||
如果你最后还有时间的话
|
||
我建议还是做一下
|
||
show off
|
||
给你的面试官录一手
|
||
比如说这里
|
||
我们可以在Payment Service
|
||
前面插入一个Risk Engine
|
||
去做一下Fraud Detection
|
||
欺诈检测
|
||
其实有很多的方案
|
||
不一定一上来
|
||
就要做什么机器学习
|
||
那最简单
|
||
你可以说
|
||
我使用一个规则引擎
|
||
比如说我加入一个规则树
|
||
或者是黑名单
|
||
白名单这样
|
||
那这种方法比较简单
|
||
门槛也比较
|
||
也便于合规和审计
|
||
但是呢
|
||
它的问题就不太适合
|
||
比较复杂的欺诈手段
|
||
那另一种比较常见的是
|
||
我使用一些统计学的方式
|
||
或者是直接上一些
|
||
简单的机器学习模型
|
||
那这类的方案
|
||
通常会受限于数据本身
|
||
对于新的欺诈手段
|
||
反应会比较慢一点
|
||
而且你也需要
|
||
手动去寻找特征
|
||
标注一下数据
|
||
大的公司
|
||
可能会使用无监督学习
|
||
来解决冷启动
|
||
或者零样本的问题
|
||
但是这种方案呢
|
||
也有一定的误报率
|
||
需要人工介入进行审核
|
||
infrastructure的支持呢
|
||
你可以上一些图模型
|
||
或者GNN这样的手段
|
||
来捕捉比较复杂的形式
|
||
这类系统的维护和开发
|
||
都会比较复杂
|
||
模型的推断和开销
|
||
也会比较大
|
||
但是金融风控力
|
||
还是比较常见的 |