系统设计:设计一个支付系统(支付宝/Stripe 级别)

支付系统是互联网最核心、最复杂的业务系统之一,直接关系到资金安全。本文从系统设计角度,拆解如何设计一个类支付宝/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), -- PENDING/PROCESSING/SUCCESS/FAILED
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), -- CASH/ESCROW/SETTLEMENT
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), -- D:借 C:贷
balance_before DECIMAL(20,4),
balance_after DECIMAL(20,4),
biz_type VARCHAR(50), -- PAYMENT/REFUND/TRANSFER
biz_id BIGINT, -- 关联业务ID
created_at DATETIME
);

流水表只追加(Append-Only): 绝不修改或删除历史记录,任何时间点的余额 = 初始余额 + Σ所有流水。

5.3 余额扣减(防超扣)

1
2
3
4
5
6
7
8
9
10
-- 乐观锁扣减余额(防并发超扣)
UPDATE account
SET balance = balance - #{amount},
version = version + 1
WHERE account_id = #{accountId}
AND balance >= #{amount} -- 余额充足
AND version = #{version}; -- 版本号一致

-- 影响行数 = 0 说明并发冲突或余额不足
-- 重试或返回失败

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),禁止跨库事务

参考资料