支付系统是互联网最核心、最复杂的业务系统之一,直接关系到资金安全。本文从系统设计角度,拆解如何设计一个类支付宝/Stripe 量级的支付平台,覆盖核心支付链路、幂等、对账、风控等关键设计。
1. 需求分析 功能需求
用户发起支付(余额支付、银行卡、第三方渠道)
支付结果查询
退款处理
账户余额管理(充值、提现)
商户资金结算
交易记录与对账
非功能需求
指标
目标
日交易量
10 亿笔/天(双十一峰值)
峰值 TPS
10 万笔/秒
支付延迟
P99 < 3 秒(含银行交互)
资金一致性
强一致,绝对不允许资金错误
可用性
99.999%(年停机 < 5 分钟)
幂等性
同一笔支付请求无论重试多少次,只扣款一次
关键挑战:
资金安全 :任何情况下不能多扣或少扣,不能重复支付
分布式一致性 :跨系统、跨数据库的资金流转保证强一致
高可用 :支付系统不可用直接导致业务损失,要求极高可用性
对账 :每天与银行、商户进行资金对账,发现并修复差错
2. 整体架构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 用户层 │ App / Web / 商户系统 ▼ [API Gateway] │ 鉴权、限流、签名验证 ▼ [支付核心服务] ├── 支付路由服务 ← 选择最优支付渠道 ├── 账户服务 ← 余额查询与扣减 ├── 订单服务 ← 支付订单管理 └── 风控服务 ← 实时反欺诈 │ ▼ [渠道适配层] ├── 支付宝渠道 ├── 微信支付渠道 ├── 银联渠道 └── 银行直连渠道 │ ▼ [第三方支付网关 / 银行核心]
3. 核心支付链路 3.1 支付流程全链路 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 用户点击"支付" │ ▼ 1. 客户端生成 client_order_id(幂等键) │ ▼ 2. API Gateway 接收请求 │ - 验证签名(防篡改) │ - 鉴权(Token 有效性) │ - 限流(防刷) │ ▼ 3. 风控实时检测(< 50ms) │ - 用户行为分析 │ - 设备指纹验证 │ - 交易金额/频次检测 │ 拒绝 → 返回拒绝原因 │ ▼ 4. 创建支付订单(幂等写入) │ - Redis NX 检查 client_order_id 是否已存在 │ - 写入 payment_order 表,状态 PENDING │ ▼ 5. 路由选择最优渠道 │ - 根据金额、银行卡类型、商户配置选渠道 │ - 渠道健康度检测(成功率 > 95%) │ ▼ 6. 调用渠道(银行/第三方) │ - 异步回调(Webhook)or 轮询 │ - 超时重试策略 │ ▼ 7. 接收支付结果 │ - 成功 → 更新订单状态,触发账务记账 │ - 失败 → 更新订单状态,释放预占资源 │ - 超时 → 状态机保持 PROCESSING,异步对账 │ ▼ 8. 账务系统记账(双边记账) │ - 借记用户账户,贷记商户账户 │ - 写入流水明细 │ ▼ 9. 通知商户(MQ 异步) - 发送支付成功/失败事件 - 商户系统更新订单状态
3.2 支付状态机 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 CREATED(已创建) │ ▼ 提交渠道 PROCESSING(处理中) │ │ │ ▼ 成功 ▼ 失败 ▼ 超时/未知 SUCCESS FAILED UNKNOWN │ │ │ 定时对账任务 │ 确认后更新 │ ▼ 用户申请 REFUNDING(退款中) │ ▼ REFUNDED(已退款)
关键原则: 支付状态只能前进,不能回退。UNKNOWN 状态由对账任务负责最终确认。
4. 幂等设计(防重复支付) 幂等是支付系统最重要的特性,确保同一笔支付无论重试多少次都只执行一次。
4.1 幂等键设计 1 2 3 4 5 6 7 8 9 客户端生成全局唯一的 idempotency_key(幂等键): 格式:{app_id}_{user_id}_{timestamp}_{random} 例: pay_1001_20241201120000_a3f5 规则: - 同一笔业务请求使用同一个 idempotency_key - 网络超时重试时,携带相同的 idempotency_key - 服务端根据 idempotency_key 去重
4.2 服务端幂等实现 1 2 3 4 5 6 7 8 9 10 11 12 方案一:Redis NX(推荐) 请求到来时: SET idempotency:{key} {request_hash} NX EX 86400 NX 保证: - 首次请求 → SET 成功 → 处理业务 - 重复请求 → SET 失败 → 返回上次结果 处理完成后: 将 value 更新为 {payment_id, status, result} 重复请求直接返回缓存的结果(不重复执行)
1 2 3 4 5 6 7 8 9 10 11 12 13 CREATE TABLE payment_order ( payment_id BIGINT PRIMARY KEY, idempotency_key VARCHAR (128 ) UNIQUE , user_id BIGINT , amount DECIMAL (20 ,4 ), currency CHAR (3 ), status VARCHAR (20 ), channel VARCHAR (50 ), channel_trade_id VARCHAR (200 ), created_at DATETIME, updated_at DATETIME );
双重保障: Redis NX 是第一道门(快速),DB 唯一约束是最终兜底(防缓存宕机)。
5. 账务系统(复式记账) 5.1 复式记账原理 支付系统采用复式记账法(Double-entry Bookkeeping) ,每笔交易都有等额的借方和贷方,借贷永远平衡。
1 2 3 4 5 6 7 8 9 用户 A 向商户 B 支付 100 元: 账户 | 借(Debit) | 贷(Credit) -------------|-----------|---------- 用户A 资产账户 | | 100(减少) 商户B 资产账户 | 100 | (增加) 平台待结算账户 | 100 | (增加,中间过渡) 复式记账保证:Σ借方 = Σ贷方,资金永远守恒
5.2 账户模型 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 CREATE TABLE account ( account_id BIGINT PRIMARY KEY, user_id BIGINT , account_type VARCHAR (20 ), balance DECIMAL (20 ,4 ), frozen_amt DECIMAL (20 ,4 ), currency CHAR (3 ), version BIGINT ); CREATE TABLE account_journal ( journal_id BIGINT PRIMARY KEY, account_id BIGINT , amount DECIMAL (20 ,4 ), direction CHAR (1 ), balance_before DECIMAL (20 ,4 ), balance_after DECIMAL (20 ,4 ), biz_type VARCHAR (50 ), biz_id BIGINT , created_at DATETIME );
流水表只追加(Append-Only): 绝不修改或删除历史记录,任何时间点的余额 = 初始余额 + Σ所有流水。
5.3 余额扣减(防超扣) 1 2 3 4 5 6 7 8 9 10 UPDATE accountSET balance = balance - #{amount}, version = version + 1 WHERE account_id = #{accountId} AND balance >= #{amount} AND version = #{version};
6. 分布式事务设计 支付涉及多个系统(支付服务、账务服务、通知服务),必须保证跨系统一致性。
6.1 两阶段提交(2PC)的问题 1 2 3 4 5 问题: Prepare 阶段所有参与者锁住资源 Commit 阶段协调者崩溃 → 参与者永久阻塞 在高并发支付场景下,2PC 性能差,且存在协调者单点故障。
6.2 Saga 模式(推荐) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 支付 Saga 步骤: Step 1: 创建支付订单(本地事务) 补偿:取消支付订单 Step 2: 扣减用户余额(账务服务) 补偿:退回用户余额 Step 3: 调用渠道支付(第三方) 补偿:发起退款 Step 4: 记录商户收款(账务服务) 补偿:扣回商户收款 Step 5: 通知商户(MQ) 补偿:发送取消通知 如果 Step 3 失败 → 逆序执行补偿: 取消商户记账 → 退回用户余额 → 取消支付订单
6.3 本地消息表(可靠消息) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 解决支付成功后,如何可靠通知商户: 1. 支付成功 → 在同一个本地事务中: - 更新 payment_order.status = SUCCESS - 写入 outbox 表(消息发件箱) INSERT INTO outbox (id, topic, payload, status) VALUES (...) 2. 后台 Outbox Processor 轮询: SELECT * FROM outbox WHERE status = 'PENDING' LIMIT 100 → 发送到 Kafka → 更新 outbox.status = SENT 3. 商户消费 Kafka 消息 → 幂等处理(按 payment_id 去重) 原子性保证:步骤1 是单库事务,步骤2 失败可以重试,最终一定投递。
7. 对账系统 对账是支付系统的最后一道防线,发现并修复资金差错。
7.1 对账类型 1 2 3 4 5 6 7 8 9 10 内部对账(实时): 支付订单表 ↔ 账务流水表 验证:每笔支付对应的流水借贷平衡 外部对账(T+1,次日): 我方交易流水 ↔ 银行/渠道流水账单 发现:渠道成功我方未处理、我方成功渠道未记录 商户对账: 平台侧商户流水 ↔ 商户侧收款记录
7.2 差错类型与处理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 差错类型: 1. 长款(平台多收): 渠道已扣款,我方未创建成功记录 处理:查找对应订单,补录成功状态,或自动退款 2. 短款(平台少收): 我方记录成功,渠道实际未扣款 处理:人工介入,向用户重新收款或标记坏账 3. 悬账(状态未知): 支付状态 UNKNOWN,渠道有记录 处理:以渠道状态为准,更新本地状态 对账流水线: [渠道文件] → 解析 → 存储 [本地流水] → 导出 → 对比 → 差异报告 → 人工审核 → 补账/退款
7.3 对账系统架构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 T+1 对账批处理(每天凌晨): 渠道账单文件(SFTP/API) │ ▼ 下载 & 解析 [对账数据仓库(Hive/Spark)] │ │ │ 本地流水导出 │ 渠道流水导入 ▼ ▼ [Spark Join 对比(按 channel_trade_id 关联)] │ ▼ [差异记录表] │ ▼ 自动处理 ├── 金额差异 < 1分 → 自动平账(浮点精度) ├── 长款 & 金额可追溯 → 自动补单 └── 无法自动处理 → 人工差错工单
8. 风控系统 8.1 实时风控(< 50ms) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 每笔支付请求经过实时风控引擎: 输入特征: ├── 用户维度:账号年龄、历史交易次数、地理位置 ├── 设备维度:设备指纹、IP 归属地、是否越狱/Root ├── 交易维度:金额、商户类别、时间(深夜异常) └── 行为维度:操作频率、鼠标轨迹(Web 端) 规则引擎(快速拦截): ├── 黑名单:命中直接拒绝 ├── 频率限制:同账号 1 分钟内 > 3 笔 → 人工验证 ├── 金额规则:单笔 > 5 万 → 短信验证 └── 地域风险:异地登录后大额支付 → 人脸识别 ML 模型(复杂欺诈检测): ├── 梯度提升树(XGBoost): 规则难以覆盖的欺诈模式 ├── 图神经网络(GNN): 团伙欺诈检测(账号关联图) └── 实时特征服务(Redis): 滑动窗口聚合特征,<5ms
8.2 异步风控(事后检测) 1 2 3 4 5 6 7 8 9 某些欺诈模式无法实时发现: ├── 账号盗用:盗用者行为模式类似正常 ├── 洗钱链路:单笔正常,多笔串联可疑 └── 团伙欺诈:单个账号正常,关联账号异常 事后分析: Kafka 消费支付流水 → Flink 实时计算 → 发现异常 → 冻结账户 + 人工审核 (允许延迟几分钟,换取更高检出率)
9. 高可用设计 9.1 支付核心可用性 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 99.999% 可用性 = 年停机 < 5 分钟 多活架构(同城双活 + 异地灾备): [同城机房A] [同城机房B] 支付服务集群 ←→ 支付服务集群 账务DB主库 → 账务DB从库 │ ↓(同步复制) [异地机房C] 灾备集群(冷备,RTO 15分钟) 流量分配: - 正常:A/B 各承担 50% 流量 - A 故障:全部流量切至 B(< 30 秒自动切换) - 同城全故障:切换至异地(人工介入,15 分钟)
9.2 渠道降级策略 1 2 3 4 5 6 7 8 9 10 11 12 13 14 单一渠道故障时的降级策略: 主渠道(支付宝)健康度 < 95% → 路由切换到备用渠道(微信支付) 所有渠道故障(极端情况): → 降级到余额支付(用户预充值余额) → 降级到账期支付(先消费后还款,需授信) 渠道健康度监控: - 每 30 秒探测渠道成功率 - 成功率 < 95% → 开始分流 - 成功率 < 80% → 全部切走 - 成功率恢复 > 99% → 逐步切回
9.3 数据库设计 1 2 3 4 5 6 7 8 9 10 11 12 读写分离 + 分库分表: 分片键:user_id - payment_order:按 user_id % 16 → 16 库,每库 16 表 = 256 张表 - account_journal:按 account_id % 16 全局流水 ID:Snowflake(趋势递增,利于 B-Tree 索引) 特殊处理: - 账务表:单独部署,使用更高配置的 DB 实例 - 账务 DB 使用同步复制(RPO=0),普通 DB 可用半同步 - 账务操作禁止跨库事务,必须在单库内完成
10. 退款设计 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 退款流程: 1. 用户申请退款 → 创建退款单,状态 REFUND_PENDING → 验证退款资格(退款期限内?金额不超原单?) 2. 原路退款 → 调用渠道退款接口 → 渠道将资金退回用户的原支付账户 3. 更新状态 → 退款成功 → 更新原支付单状态 → 反向记账(冲销原流水) 退款幂等: 同一退款单只能退款一次(DB 唯一约束) 退款 ID 作为幂等键调用渠道 部分退款: 支持多次部分退款,累计退款金额 ≤ 原支付金额 超额退款请求直接拒绝
11. 存储选型总结
数据
选型
理由
支付订单
MySQL(分库分表)
强一致,支持事务,按 user_id 分片
账户余额
MySQL(主库读写)
余额精确计算,禁止走从库
账务流水
MySQL(append-only)
审计溯源,只追加不修改
幂等键
Redis + MySQL 唯一索引
Redis 快速去重,DB 兜底
风控特征
Redis(滑动窗口)
毫秒级聚合查询
黑名单
Redis Bloom Filter
亿级黑名单,内存高效
支付事件
Kafka
高吞吐,解耦支付与通知
对账数据
Hive / Spark
T+1 批处理,PB 级对账
对账结果
MySQL
差错工单管理
渠道配置
ZooKeeper / 配置中心
实时推送,支持动态路由
12. 面试要点总结
维度
关键设计
幂等
客户端生成 idempotency_key,Redis NX + DB 唯一索引双重保障
一致性
Saga 编排 + 本地消息表(Outbox),避免 2PC
账务
复式记账,流水只追加,任意时点可重算余额
防超扣
账户乐观锁(version CAS),余额充足校验
对账
T+1 批处理对账,差错分类自动/人工处理
风控
实时规则引擎(< 50ms)+ 异步 ML 检测
高可用
同城双活 + 异地灾备,渠道故障自动切换
状态机
支付状态严格单向,UNKNOWN 由对账任务最终确认
退款
原路退款,退款单幂等,部分退款不超原单金额
数据库
账务 DB 同步复制(RPO=0),禁止跨库事务
参考资料