支付系统设计

支付系统是金融级严肃场景的代表——钱不能丢、不能多、不能少。设计支付系统意味着在分布式环境下保证精确的余额一致性和事务原子性。

一、核心需求

需求 说明
精确性 一分钱不能多、一分钱不能少
幂等性 同一笔支付请求重试不能重复扣款
原子性 扣款和入账必须同时成功或同时失败
可审计 每笔交易有完整的流水记录和凭证
低延迟 支付确认在秒级完成
高可用 99.99%+ 可用性,不能因为支付故障影响交易

二、幂等性设计

支付系统最核心的要求:同一笔支付请求,重试多少次都只能成功扣款一次

2.1 幂等键

每笔支付请求携带一个唯一的幂等键(Idempotency Key),由客户端生成:

1
2
3
4
5
6
7
8
POST /v1/payments
Idempotency-Key: 8f7d3b2a-1c4e-4a5b-9d6e-3f8a7c1b2d4e
{
"amount": 10000,
"currency": "CNY",
"source": "card_xxx",
"destination": "merchant_yyy"
}

2.2 服务端幂等实现

1
2
3
4
5
6
7
8
1. 收到请求 → 检查 Idempotency Key 是否已存在
├─ 不存在 → 继续处理
└─ 已存在 → 返回之前的结果(不重复执行)
2. 处理支付:
a. 写入 Idempotency Key → Redis (NX, TTL=24h)
b. 执行扣款逻辑
c. 更新 Idempotency Key 的结果状态
3. 返回结果

关键注意:如果第 2 步执行到一半服务器宕机,重启后客户端重试(携带相同 Idempotency Key),服务端需要能够恢复并继续之前未完成的交易。这要求支付状态持久化到数据库,而非仅依赖 Redis。

三、分布式事务

3.1 为什么支付需要分布式事务

一次用户支付涉及多个系统的状态变更:

1
2
3
4
5
用户支付 100 元购买商品
├─ [用户账户服务] 余额 -100
├─ [商家账户服务] 余额 +97 (平台抽 3%)
├─ [积分服务] 用户积分 +10
└─ [订单服务] 订单状态 → 已支付

这四个操作必须在整体上满足原子性——要么全部成功,要么全部不执行。

3.2 方案选型

方案 适用场景 复杂度
TCC (Try-Confirm-Cancel) 强一致性要求
Saga (补偿事务) 长事务
本地消息表 需要异步解耦
两阶段提交 (2PC) 数据库层面 低(但性能差)

支付场景通常用 TCC 模式:

Try 阶段(预留资源):

1
2
3
用户账户服务:冻结 100 元(余额 500 → 可用 400,冻结 100
商家账户服务:(无需操作,Confirm 时才入账)
积分服务:(无需操作,Confirm 时才加积分)

Confirm 阶段(确认执行):

1
2
3
用户账户服务:解冻并扣除 100 元(冻结 100 → 余额 400)
商家账户服务:余额 +97
积分服务:用户积分 +10

Cancel 阶段(回滚):

1
用户账户服务:解冻 100 元(可用 400 → 500)

3.3 空回滚与悬挂

分布式事务的两个经典异常:

空回滚(Empty Rollback):Cancel 请求先于 Try 请求到达。服务端需要识别出”从未执行过此 Try”并忽略 Cancel。

防悬挂(Prevent Hanging):确认 Try 已超时被 Cancel,但延迟的 Try 请求又到达了。服务端需要识别出”此 Try 已被 Cancel”并拒绝执行。

两者都需要通过记录事务状态来实现:在执行任何操作前先检查事务日志中的 TCC 阶段状态。

四、账户模型

4.1 复式记账

支付系统最基础的账户模型——每笔交易都有借方和贷方,借贷必相等:

1
2
3
4
5
6
7
交易 ID: TXN-001
─────────────────────────
: 用户A账户 -¥100.00
: 商家B账户 +¥97.00
: 平台收入账户 +¥3.00
─────────────────────────
合计: 借方 -100 + 贷方 100 = 0 ✓

4.2 账户表设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
accounts 表:
id BIGINT PRIMARY KEY
user_id BIGINT
balance DECIMAL(18,2) -- 当前余额
frozen DECIMAL(18,2) -- 冻结金额
version INT -- 乐观锁版本号
updated_at TIMESTAMP

transactions 表:
id BIGINT PRIMARY KEY
txn_id VARCHAR(64) UNIQUE -- 幂等键
from_account BIGINT
to_account BIGINT
amount DECIMAL(18,2)
status ENUM('PENDING','SUCCESS','FAILED','CANCELLED')
txn_type ENUM('DEBIT','CREDIT')
idempotency_key VARCHAR(64) UNIQUE
created_at TIMESTAMP

余额更新使用乐观锁:

1
2
3
UPDATE accounts 
SET balance = balance - 100, version = version + 1
WHERE id = ? AND version = ? AND balance >= 100;

如果 version 不匹配(被其他事务修改)或余额不足,更新失败,上层重试。

五、风控系统

支付系统的风控需要在 100 毫秒以内给出决策。

5.1 风控检测维度

维度 规则示例
用户行为 短时间内多次支付、异地登录后支付
金额异常 单笔金额远超历史均值
设备指纹 新设备、模拟器、Root/越狱设备
网络环境 IP 黑名单、代理/VPN 检测
关联风险 与已知欺诈账户的设备/IP 重合

5.2 架构

1
2
3
4
5
6
7
8
9
10
11
12
支付请求

[规则引擎] ← 预定义规则(快速过滤,< 10ms)
│ ├─ 通过 → 继续支付
│ ├─ 拒绝 → 返回风控拦截
│ └─ 待定 →

[机器学习模型] ← 复杂的风险评估(100ms 内)
│ ├─ 低风险 → 继续支付
│ └─ 高风险 →

[人工审核] ← 异步处理,不阻塞支付流程

六、对账系统

对账是支付系统的最后一道防线:每天凌晨银行返回清算文件,与内部交易流水做逐笔比对。

6.1 对账流程

1
2
3
4
5
6
7
8
1. 获取银行清算文件(通常是 CSV/XML)
2. 解析并标准化为统一格式
3. 与内部 transactions 表按交易号逐笔比对
├─ 匹配 → 标记为已对账
├─ 银行有、内部无 → 长款(可疑)
└─ 内部有、银行无 → 短款(可疑)
4. 差异数据进入人工处理工作流
5. 对账完成后生成对账报告

七、小结

支付系统的核心原则:一秒的不一致 = 钱的损失。幂等性保证不重复扣款,TCC 分布式事务保证跨服务原子性,复式记账保证每笔交易可追溯,风控系统在 100ms 内拦截欺诈。这些设计模式不仅适用于支付,在任何涉及资金、库存、积分等需要精确计数的系统中都同样适用。