第1篇:应付亿级用户规模的支付系统代码实战-开篇词

59 阅读16分钟

「提醒」:是付费专栏,但是在知识星球里是免费的。目前星球里已更新了「几亿用户,百万并发的C端商品系统实战」、「DDD领域驱动设计三年落地实战」和「应付亿级用户规模的支付系统代码实战」,后续的订单,支付,结算和购物车专栏,星球内也都是免费的。

有一天晚上接近12点,订单系统的同事在群里发了一条消息:今天有几笔自由卡订单状态不对劲,订单上写着已支付,但支付系统里查不到对应的支付流水。

排查的结果让人背后发凉。有人通过技术手段伪造自由卡支付回调的请求格式,自己构造了参数,直接调了订单系统的回调接口。订单系统收到回调通知,以为用户付过钱了,订单状态从待支付变成了已支付,后续的履约流程全部被触发。微信和支付宝这种基本不可能伪造。但是自由卡支付是内部支付,稍有不慎,就会出安全故障的。

这笔钱从来没有真正到账。还好当时那个人胆子小,不敢搞多,不然就是很大的资损故障了。

这件事当天紧急修复了。但它暴露了一个更深层的问题:订单系统和支付系统之间缺少一道每日交叉对账。如果几个小时前就能发现这笔差异,资损窗口会被压缩到最小。当然,那会的支付系统的安全性也是不够的。

后来我们在支付系统里加上了两套对账机制:一套和微信、支付宝等渠道对账,确保平台账单和渠道账单一致;另一套和订单系统对账,每天把订单记录的支付流水号逐条比对,任何一个对不上的差异都会告警,人工介入核查。

这个专栏给的内容,就是我当年作为技术部负责人牵头重构的这套支付系统。它支撑着超过一亿的注册用户,每天要处理平均50万笔订单的支付请求,是一套真实运行过的生产系统。接入了国内外的各种支付渠道,跑过组合支付、退款、对账、风控的完整链路,每一处设计决策背后都有实际的业务压力在推动。

这不是玩具级Demo,不是调个微信SDK就跑起来的教学系统。这是一套在亿级用户规模下扛过真实流量的支付系统,对接了微信支付、支付宝、钱包余额、自由卡、各种国外支付、POS线下扫码,覆盖了从支付请求进来到对账闭环出去的完整链路。

支付系统整体架构

和市面上讲支付的有什么不一样

市面上讲支付系统的内容不少,大部分聚焦在怎么对接微信支付、支付宝支付的API:怎么申请商户号、怎么拼装支付参数、怎么验证签名、怎么处理回调通知。看完确实能跑通一个简单的支付Demo。

但这些内容回答不了一个核心问题:当你面对的不是一个渠道,而是七八个渠道同时在线,用户还能把几种渠道组合在一起支付的时候,系统该怎么设计?

举个例子:用户买了一张自由卡折扣券抵扣了50块,又用了钱包余额里的20块,剩下的30块走微信支付。这笔交易在支付系统内部会产生几条流水记录?扣款顺序怎么定?自由卡先扣出了问题,钱包和微信要不要回滚?三个渠道的回调先后到达,怎么合并成一个结果通知订单系统?

Demo里用Redis做DECR扣个库存就完事了,但真实生产环境面对的是完全不同的难度等级。

再举个例子:对接国际支付渠道。有些的签名用的是RSA-SHA256,有些用的是HMAC-SHA256。它们的回调格式、多币种处理、退款接口的参数结构和国内渠道差异很大。你不是调一个SDK就完了,你得设计一套统一的抽象层,把这些差异封装起来,让上游的支付编排逻辑不用关心到底在对接哪个渠道。

还有退款。部分退款比全额退款复杂得多。一笔组合支付的订单要退其中的一部分,这笔钱从哪个渠道退、退多少、扣款顺序能不能倒过来当退款顺序用。退款接口调用成功后回调和主动查询结果不一致怎么办。这些在Demo里碰不到的问题,在生产环境里每一个都对应着一次线上事故或者一次紧急修复。

这个专栏给你的,不是某个支付渠道的API对接文档重写,而是一套完整的、在多渠道和多支付方式组合场景下验证过的支付系统工程方案。

支付渠道全景

专栏会怎么展开

整个专栏共20篇,按支付请求的生命周期完整覆盖:从最开始的PRD和技术方案讲清楚要做什么、为什么这么做,然后落地到支付网关、渠道路由、各渠道对接、组合支付拆解、退款体系、回调幂等,再到风控拦截和对账闭环。 专栏模块总览

下面逐一把每个模块的重点列出来。

模块一:需求与方案设计(3篇)

第2篇写支付系统需求PRD。这块很多人觉得是走形式,但真要动手之前把需求捋清楚,后面少走很多弯路。线上支付有哪些场景(APP里支付、小程序里支付、门店POS扫码支付),线下POS支付和线上支付的差异在哪。支付方式不只是一个微信支付打天下,要考虑微信支付、支付宝、钱包余额、自由卡,以及在门店里用POS机扫码支付的授权码模式。组合支付的需求怎么梳理,退款有哪些场景(全额退、部分退、多笔合并退),对账是对哪些账,什么是多收款主体和多商户号管理。这些点如果一开始没想透,后面架构上就会不断出现需要填坑的地方。

第3篇写支付系统的整体技术方案。这里会给出完整的分层架构设计,画出整体架构图。接入层怎么隔离不同终端的差异,编排层怎么把一次支付请求拆成多个渠道的调用并协调它们的结果,渠道层怎么做到一套接口适配所有支付渠道。还会展开讨论几个关键的架构决策:渠道隔离和统一支付之间的矛盾在哪里,组合支付编排用什么模式最合适,为什么不用工作流引擎而是用策略和模板方法。技术方案的结论都是从前面的需求推导出来的,你看到的不只是一个架构图,而是这个架构图怎么被一步步推出来的。

模块二:基础设施(2篇)

第4篇设计支付流水数据模型。这是一个地基问题,一旦建歪了后面怎么弥补都很别扭。主流水加明细的双层结构怎么设计,为什么不能用单表存所有信息。支付状态机的状态怎么定义:新建、处理中、成功、失败、冲正,各个状态之间的流转规则是什么,哪些转移是被禁止的。组合支付场景下,一条主流水关联多条渠道明细,数据和状态怎么做到原子落库。分表的路由索引表怎么设计,为什么分表键要选支付流水号而不是用户ID。

第5篇写商户与支付配置体系。三层配置模型(应用层、商户层、门店层)的详细设计,对应payment_merchant、payment_application、payment_merchant_store三张表。多商户号场景下的配置查找链:一个支付请求进来,怎么根据请求来源和门店信息找到对应的商户号和密钥。这部分是渠道路由和签名的前置依赖,如果不理解配置怎么加载,看到渠道对接那一章会被各种配置项绕晕。

模块三:核心支付链路(12篇)

这是专栏最核心的部分,占了12篇。

第6篇写支付网关。支付网关和基础网关的本质区别在哪:基础网关做流量转发,支付网关还要做参数补全和渠道信息补充,比如把用户的openId和unionId补齐。接入路由是怎么根据请求来源分发的,签名验证的机制放在网关层还是放到每个渠道处理器里做,各有什么利弊。

第7篇写支付编排与渠道路由。策略模式实现渠道路由,支付渠道处理器怎么注册和发现。费用分摊的计算逻辑:自由卡优先、钱包次之、第三方渠道兜底,这个顺序是怎么推出来的。支付编排服务的三大职责是拆解一次组合支付请求、协调多个渠道的调用结果、合并多个回调为一个统一的支付结果。

第8篇写国内渠道对接。微信支付和支付宝在对接上的异同:SDK集成方式的差异,MD5和RSA2两套签名体系的对比,预下单加支付确认的两阶段模式。国内渠道的回调格式和验签方式各有特点,抽象层要预留哪些扩展点。

第9篇写钱包支付。早期钱包是外包团队做的,后来自己重新做了一套。从外包切换到自研,怎么做到平滑过渡、两台账务体系怎么对齐、支付中心怎么做到不感知底层钱包的变更。

第10篇写自由卡支付。自由卡在组合支付里承担了抵扣优先级最高的角色,因为它的成本最低。购买和激活阶段怎么做库存扣减和分布式锁,预下单时怎么冻结金额,支付阶段怎么完成实际扣款,支付失败时怎么自动冲正退回余额。这条链路上每一个节点的异常都要兜住,不然钱会莫名其妙消失。

第11篇写实物订单支付与履约分流。这是支付系统里一个容易忽略但实际非常重要的设计点:虚拟订单和实物订单在支付回调、履约触发上的处理链路完全不同。虚拟订单支付成功后只需轻量通知,自由卡充值或优惠券发放即时完成、订单直接关闭。实物订单支付成功后要把支付网关返回的完整交易数据(渠道流水号、买家OpenId)透传给仓储系统触发发货,订单进入待发货状态。两套回调通知用的是同一个策略路由框架(按订单类型加支付渠道做二级匹配),但下游各自走各自的线程池和通知端点。组合订单更复杂一点:一笔订单里同时有虚拟和实物商品,支付回调要同时通知两个履约端。

第12篇写国际支付渠道对接。国际支付渠道各有各的签名算法和回调格式。和国内渠道最不一样的地方在于多币种和跨境支付,汇率在哪里算、手续费谁来承担、退款时汇率变动了怎么处理。国际渠道没有一个统一的SDK可以套,全部走的是HTTP裸调加鉴权头,每个对接细节都值得单独展开。

第13篇写POS线下支付。授权码模式是POS支付特有的:用户在门店扫码,系统拿到授权码(payAuthCode)后再发起支付下单,授权码存在Redis里,30秒后自动失效。POS支付的处理中状态(国际支付渠道返回的TS03)需要前端轮询查询,和线上支付的用户体验完全不同。POS场景下也支持多支付方式组合,比如先用一张自由卡抵扣,剩下的在POS机上扫微信付掉。

第14篇写组合支付。这是整个系统里最复杂的部分。主流水加明细双层数据的写入和更新策略必须原子化,不能出现主流水成功明细失败或者反过来。扣款优先级的规则设计(自由卡到钱包到第三方),费用分摊算法。多渠道回调合并机制用的是Redis的Hash结构管理各渠道的回调状态,所有渠道都回调成功后才把合并结果通知订单系统。部分退款时的渠道优先级和正向支付的扣款顺序不完全一致,为什么。

第15篇写退款体系。退款状态机和支付状态机的关联关系。AbstractOrderRefundTemplate用模板方法模式统一各渠道的退款流程,渠道差异部分在子类里通过抽象方法扩展。退款接口用Redis分布式锁保证幂等,避免同一个退款请求被处理多次。部分退款的金额计算和渠道退款的优先级规则。

第16篇写支付回调与幂等设计。异步回调是支付系统里的最大不稳定性来源:回调可能延迟、可能丢、可能重复。三层幂等保证怎么落到各个渠道上:自由卡购买层、账户交易层、组合支付层,各用各的幂等键,互不干扰。回调合并的时序问题怎么处理:先到的先用,后到的等一等,太晚到的主动查询渠道确认。回调失败的重试和补偿策略。

第17篇写支付风控。风控放在模块三的最后一篇,因为它覆盖所有渠道,是横向切面。先熟悉各个渠道的支付流程,再回头看拦截层怎么做决策,更容易理解。黑名单缓存怎么设计,黑名单的匹配逻辑是怎么根据用户ID、IP、设备指纹来组合判断的。接入了数美和腾讯云两套第三方风控,每家的调用时机和权重不一样,怎么整合它们的返回结果做最终的风险决策。风控拦截后不是粗暴地拒绝,不同风险等级对应不同的用户体验:低风险放行、中风险加验证码、高风险直接拦截。

模块四:对账(2篇)

第18篇写支付渠道对账。BillReader工厂模式怎么做到一套代码适配微信和支付宝不同的对账文件下载接口。账单文件(一般是CSV或者文本格式)怎么解析成统一的内部格式。对账差异的类型:平台有渠道没有、渠道有平台没有、两边都有但金额对不上。自动对账的差异处理流程和人工介入的机制。

第19篇写订单与支付系统对账。这是开篇提到的那个事故的直接产物。chargeBill对比支付系统的账单,chargeOrder对比订单系统的支付流水号,chargeDiff锁定差异记录。退款方向的refundBill、refundOrder、refundDiff做同样的三层检查。差异记录表的设计,每种差异类型的处理优先级和人工处理流程。这套对账机制的价值不在于发现差异,而在于把差异发现的时间窗口从几天压缩到几小时。

完结篇

第20篇对整个专栏做一次串联回顾。把支付网关、编排路由、各渠道对接、组合支付、退款、回调、风控、对账这一整条链路串起来,形成完整的支付系统全局认知。

技术栈

整个支付系统用到的技术组件和版本:

类别技术版本
语言Java17
核心框架Spring Boot2.7
微服务Spring Cloud Alibaba2021.0.1.0
RPCDubbo3.2
注册中心/配置中心Nacos随Spring Cloud Alibaba版本
限流熔断Sentinel随Spring Cloud Alibaba版本
ORMMyBatis Plus3.5
分表ShardingSphere5.3
数据库MySQL8.0
缓存/分布式锁Redis + RedissonRedisson 3.17
消息队列RocketMQSpring Boot Starter 2.2
定时任务XXL-Job2.4
对象映射MapStruct1.5
本地缓存Caffeine2.9

服务间通过Dubbo做RPC调用,Nacos同时承担注册中心和配置中心的角色。消息队列主要负责支付回调的异步处理和延迟支付超时检查。

小结

支付系统就是一个绝佳的决策训练场。你面前有七八个支付渠道,每个渠道有自己的对接方式、自己的签名机制、自己的回调格式。用户能把几种渠道组合在一起用,退款的时候还能只退其中一种。订单系统和支付系统之间每天要对一次账,少了一笔就可能意味着资损。风控要在所有渠道的入口拦截,但不能粗暴到把正常用户也挡在外面。

这些问题都没有现成的标准答案。你必须在自己的业务场景下做出取舍,并且承担取舍带来的后果。

这个专栏的价值,不是告诉你看哪个支付渠道的官方文档,而是让你看到一个真实的支付系统从架构设计到代码落地的全过程,看到每个技术选择背后的推演和代价。当你自己面对类似场景时,能更快地做出符合自己业务特点的判断。

每周更新两篇。早鸟价是59.9元,只有20个名额,后续恢复原价99.9元。

所有的代码都可以在知识星球里获取。「应付亿级用户规模的支付系统代码实战」这个专栏在星球里是免费的,也可以接受无限次的咨询。后续新写的所有付费专栏,在知识星球里都是免费的。我的星球是:

  • 老码头的技术浮生录

如果你想单独订阅,也可以的。专栏发布在知乎里了,我的知乎是:

  • SamDeepThinking