系统设计:支付系统
支付系统是金融系统的核心,要求准确性 > 可用性 > 性能。一分钱都不能错。
核心需求
支付的基本流程
用户下单
│
▼
创建支付订单(状态:INIT)
│
▼
调用支付渠道(微信/支付宝/银行卡)
│
├── 成功 → 支付订单状态: SUCCESS → 通知业务系统 → 触发履约
│
├── 失败 → 支付订单状态: FAILED → 通知用户 → 可重试/换渠道
│
└── 处理中 → 支付订单状态: PROCESSING → 定时轮询查询结果
非功能性需求
| 需求 | 要求 | 说明 |
|---|---|---|
| 精确性 | 100% | 资金操作必须完全准确,不可丢失、不可重复 |
| 一致性 | 最终一致 | 超时场景允许短暂不一致,但必须可修复 |
| 幂等性 | 全局幂等 | 同一笔支付请求重复提交只扣款一次 |
| 可用性 | 99.99% | 支付核心链路降级后仍能完成核心功能 |
| 可追溯性 | 全链路审计 | 每笔资金操作都有完整记录,支持反向追查 |
状态机设计
支付订单的状态转换必须严格管控,防止非法状态迁移。
核心状态
┌───────────┐
│ INIT │ ← 初始化
└─────┬─────┘
│ 提交支付
┌───────────┼───────────┐
▼ ▼ ▼
┌─────────┐ ┌──────────┐ ┌─────────┐
│PROCESSING│ │ SUCCESS │ │ FAILED │
└────┬────┘ └──────────┘ └─────────┘
│
查询结果
│
┌────────┴────────┐
▼ ▼
┌─────────┐ ┌─────────┐
│ SUCCESS │ │ FAILED │
└────┬────┘ └─────────┘
│
▼
┌─────────┐ ┌─────────┐
│ REFUND │────→│REFUNDED │
│REQUESTED│ │ │
└─────────┘ └─────────┘
状态转换表
| 当前状态 | 允许迁移到 | 触发条件 |
|---|---|---|
| INIT | PROCESSING | 用户提交支付 |
| INIT | FAILED | 参数校验失败 |
| PROCESSING | SUCCESS | 渠道回调成功 |
| PROCESSING | FAILED | 渠道回调失败/超时 |
| SUCCESS | REFUND_REQUESTED | 用户发起退款 |
| REFUND_REQUESTED | REFUNDED | 退款成功 |
| REFUND_REQUESTED | REFUND_FAILED | 退款失败(可重试) |
幂等性设计
为什么支付必须幂等?
场景:用户点击支付按钮,由于网络抖动,请求发了两次
请求 1: 创建支付 → 扣款成功 → 返回 SUCCESS(但响应丢失)
请求 2: 创建支付 → 再次扣款 → 返回 SUCCESS
结果:用户被扣了两次钱!(严重事故)
幂等性实现方案
方案 1:支付 Token(推荐)
1. 用户点击支付前,前端先向服务端申请一个支付 Token(幂等键)
2. 服务端生成唯一 Token,写入 Redis(TTL=15min)
3. 用户提交支付时带上 Token
4. 服务端以 Token 作为幂等键:
- Token 存在且未使用:执行支付,标记 Token 为已使用
- Token 已使用:返回已存在的支付结果
- Token 不存在:拒绝请求
import redis
class PaymentService:
def __init__(self, redis_client):
self.r = redis_client
def generate_token(self, order_id: str) -> str:
"""生成支付 Token"""
token = f"pay_token:{uuid.uuid4()}"
self.r.setex(token, 900, f"order:{order_id}") # 15min TTL
return token
def process_payment(self, token: str, amount: int, user_id: str):
"""处理支付请求,保证幂等"""
# 1. 检查 Token
token_key = f"token_status:{token}"
token_status = self.r.get(token_key)
if token_status == b"used":
# 已处理过,返回缓存的结果
return self.get_payment_result(token)
if token_status == b"processing":
# 正在处理中,返回处理中状态
return {"status": "PROCESSING"}
# 2. 设置处理中状态(带过期时间,防止死锁)
if not self.r.set(token_key, "processing", nx=True, ex=60):
return {"status": "PROCESSING"}
try:
# 3. 执行真正的支付逻辑
result = self.execute_payment(amount, user_id)
# 4. 标记 Token 为已使用,缓存结果
self.r.setex(token_key, 86400, "used") # 缓存 1 天
self.r.setex(f"result:{token}", 86400, json.dumps(result))
return result
except Exception as e:
# 异常时删除 processing 状态,允许重试
self.r.delete(token_key)
raise
方案 2:数据库唯一约束
create table payment_orders (
id bigint primary key auto_increment,
idempotency_key varchar(64) unique not null, -- 幂等键
order_id varchar(64) not null,
amount decimal(18, 2) not null,
status varchar(20) not null,
created_at timestamp default now(),
unique key uk_idempotency (idempotency_key)
);
-- 重复提交时因唯一键冲突而失败,捕获异常返回已有结果
分布式事务方案
支付涉及多个系统的状态变更(支付订单、账户余额、业务订单),需要保证一致性。
方案 1:TCC(Try-Confirm-Cancel)
Try 阶段:
- 支付服务:创建支付订单,状态为 PENDING,冻结用户余额
- 账户服务:冻结对应金额
- 业务服务:标记订单为支付中
Confirm 阶段(支付成功):
- 支付服务:更新订单为 SUCCESS
- 账户服务:扣减冻结金额
- 业务服务:触发履约流程
Cancel 阶段(支付失败/超时):
- 支付服务:更新订单为 FAILED
- 账户服务:解冻金额
- 业务服务:恢复订单为待支付
优点:强一致性,业务层控制
缺点:业务侵入性强,实现复杂
方案 2:Saga 模式
正向流程(每个步骤都是一个本地事务):
T1: 创建支付订单
T2: 调用渠道扣款
T3: 更新业务订单状态
T4: 发送支付成功通知
补偿流程(如果 T3 失败,回滚 T1、T2):
C3: (T3 失败了,不需要补偿——数据没变)
C2: 调用渠道退款(冲正)
C1: 关闭支付订单
优点:适合长事务,无需全局锁
缺点:补偿可能失败,需要人工介入
方案 3:本地消息表(最常用)
-- 支付服务本地事务中,同时写入业务表和消息表
create table payment_message (
id bigint primary key auto_increment,
business_type varchar(50), -- 业务类型:PAY/REFUND
business_id varchar(64), -- 业务ID
payload json, -- 消息内容
status varchar(20), -- PENDING/SENT/SUCCESS
retry_count int default 0,
created_at timestamp default now(),
next_retry_time timestamp
);
流程:
- 支付成功时,在本地的同一个事务中:
- 更新支付订单状态为 SUCCESS
- 插入一条消息到
payment_message表(状态 PENDING)
- 独立的消息投递服务定时扫描
payment_message表:- 将 PENDING 消息发送到 MQ
- 更新状态为 SENT
- 业务系统消费 MQ 消息,处理成功后回调确认
- 消息投递服务收到确认,更新状态为 SUCCESS
优点:实现简单,可靠性高
缺点:有延迟(秒级),不适合实时性要求极高的场景
支付渠道路由
多渠道架构
用户发起支付
│
▼
┌─────────────┐
│ Route Engine │ ← 根据规则选择最优渠道
└──────┬──────┘
│
┌───┴───┬────────┬────────┐
▼ ▼ ▼ ▼
微信 支付宝 银联 Apple Pay
路由策略
| 策略 | 说明 | 场景 |
|---|---|---|
| 成本优先 | 选择手续费最低的渠道 | 大额转账 |
| 成功率优先 | 选择历史成功率最高的渠道 | 支付核心链路 |
| 速度优先 | 选择响应最快的渠道 | 小额快捷支付 |
| 用户偏好 | 选择用户上次使用的渠道 | 提升体验 |
| 渠道挡板 | 某渠道故障时自动切换 | 容灾 |
渠道降级:
def route_payment(amount: int, user_id: str, user_preference: str):
channels = get_available_channels()
# 1. 用户偏好
if user_preference in channels:
return user_preference
# 2. 规则匹配
for rule in routing_rules:
if rule.matches(amount, user_id):
channel = rule.select_channel(channels)
if channel.is_healthy():
return channel
# 3. 默认兜底
return channels.get_fallback()
对账体系
对账是发现数据不一致的最后防线。
对账类型
| 对账 | 双方 | 频率 | 用途 |
|---|---|---|---|
| 支付订单 vs 渠道账单 | 系统内部 vs 微信/支付宝 | 每日 | 发现漏单、金额差异 |
| 支付订单 vs 业务订单 | 支付服务 vs 业务系统 | 实时/每日 | 发现状态不一致 |
| 账户流水 vs 余额 | 流水明细 vs 账户余额 | 每日 | 发现金额计算错误 |
对账流程
T+1 日 3:00 AM
│
▼
下载渠道对账单(微信/支付宝提供的前日交易明细)
│
▼
┌────────────────────────────────────────┐
│ 对账引擎 │
│ 1. 以渠道订单号为 key,比对双方数据 │
│ 2. 分类结果: │
│ - 长款:渠道有,我方无 │
│ - 短款:我方有,渠道无 │
│ - 金额不符:双方金额不一致 │
│ - 状态不符:双方状态不一致 │
│ - 一致:正常 │
└────────────────────────────────────────┘
│
▼
生成差异报表 → 人工确认/自动修复 → 平账
风控体系
实时风控
支付请求
│
▼
┌──────────────┐
│ Risk Engine │
│ 规则引擎 + ML │
└──────┬───────┘
│
┌───┴───┐
▼ ▼
通过 拦截/增强验证
规则示例:
- 同一 IP 1 分钟内支付超过 5 笔 → 拦截
- 同一设备短时间内更换多个支付账户 → 增强验证(短信/人脸)
- 异地登录后的大额支付 → 增强验证
- 凌晨 3-5 点的高频支付 → 人工审核
风控分层
| 层级 | 策略 | 响应时间 |
|---|---|---|
| L1 规则引擎 | 基于规则的硬拦截 | < 10ms |
| L2 机器学习 | 异常检测模型 | < 50ms |
| L3 人工审核 | 高风险订单人工确认 | 分钟级 |
面试答题框架
第一步:强调核心原则(30秒)
支付系统最重要的是精确性 > 可用性 > 性能。资金操作必须完全准确,宁可拒绝支付也不能错误扣款。
第二步:状态机 + 幂等性(2分钟)
严格的状态机防止非法迁移 + Token/唯一键保证幂等,同一笔请求重复提交只扣一次款。
第三步:分布式事务(2分钟)
根据一致性要求选择方案:强一致性用 TCC,长事务用 Saga,高可用用本地消息表。最常用的是本地消息表 + MQ。
第四步:渠道与对账(2分钟)
渠道路由(成本/成功率/用户偏好)+ 降级策略。T+1 对账发现差异(长款/短款/金额不符),自动修复或人工介入。
第五步:风控(1分钟)
规则引擎 + 机器学习分层,实时拦截可疑交易。
常见问题
Q:如果渠道回调丢失,怎么保证支付状态正确?
超时主动查询:支付提交后进入 PROCESSING 状态,定时任务(如每 30 秒)轮询渠道查询接口,直到有明确结果或超过最大超时时间(如 30 分钟)。超时后标记为 FAILED,允许用户重新支付。
Q:退款失败怎么办?
退款是可重试的,失败后进入重试队列(指数退避)。如果超过最大重试次数仍失败,转人工处理。
Q:如何防止内幕人员篡改支付记录?
- 敏感操作需要多人审批;2. 支付记录不可物理删除,只能追加冲正记录;3. 完整的操作审计日志;4. 数据库变更通过 DBA 审核。
Q:Payment Token 和 Order ID 有什么区别?
Order ID 标识一笔业务订单(如购买商品),一个订单可能有多次支付尝试(第一次卡余额不足,换卡再付)。Payment Token 标识一次具体的支付请求,保证这次请求幂等。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。