系统设计:支付系统

支付系统架构设计详解:从支付核心流程、幂等性保证、分布式事务(TCC/Saga/本地消息表)到对账与风控体系,涵盖支付状态机、渠道路由与退款处理。

系统设计:支付系统

支付系统是金融系统的核心,要求准确性 > 可用性 > 性能。一分钱都不能错。


核心需求

支付的基本流程

用户下单
   │
   ▼
创建支付订单(状态:INIT)
   │
   ▼
调用支付渠道(微信/支付宝/银行卡)
   │
   ├── 成功 → 支付订单状态: SUCCESS → 通知业务系统 → 触发履约
   │
   ├── 失败 → 支付订单状态: FAILED → 通知用户 → 可重试/换渠道
   │
   └── 处理中 → 支付订单状态: PROCESSING → 定时轮询查询结果

非功能性需求

需求要求说明
精确性100%资金操作必须完全准确,不可丢失、不可重复
一致性最终一致超时场景允许短暂不一致,但必须可修复
幂等性全局幂等同一笔支付请求重复提交只扣款一次
可用性99.99%支付核心链路降级后仍能完成核心功能
可追溯性全链路审计每笔资金操作都有完整记录,支持反向追查

状态机设计

支付订单的状态转换必须严格管控,防止非法状态迁移。

核心状态

                    ┌───────────┐
                    │   INIT    │ ← 初始化
                    └─────┬─────┘
                          │ 提交支付
              ┌───────────┼───────────┐
              ▼           ▼           ▼
        ┌─────────┐ ┌──────────┐ ┌─────────┐
        │PROCESSING│ │  SUCCESS │ │  FAILED │
        └────┬────┘ └──────────┘ └─────────┘
             │
       查询结果
             │
    ┌────────┴────────┐
    ▼                 ▼
┌─────────┐     ┌─────────┐
│ SUCCESS │     │  FAILED │
└────┬────┘     └─────────┘
     │
     ▼
┌─────────┐     ┌─────────┐
│ REFUND  │────→│REFUNDED │
│REQUESTED│     │         │
└─────────┘     └─────────┘

状态转换表

当前状态允许迁移到触发条件
INITPROCESSING用户提交支付
INITFAILED参数校验失败
PROCESSINGSUCCESS渠道回调成功
PROCESSINGFAILED渠道回调失败/超时
SUCCESSREFUND_REQUESTED用户发起退款
REFUND_REQUESTEDREFUNDED退款成功
REFUND_REQUESTEDREFUND_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
);

流程:

  1. 支付成功时,在本地的同一个事务中:
    • 更新支付订单状态为 SUCCESS
    • 插入一条消息到 payment_message 表(状态 PENDING)
  2. 独立的消息投递服务定时扫描 payment_message 表:
    • 将 PENDING 消息发送到 MQ
    • 更新状态为 SENT
  3. 业务系统消费 MQ 消息,处理成功后回调确认
  4. 消息投递服务收到确认,更新状态为 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:如何防止内幕人员篡改支付记录?

  1. 敏感操作需要多人审批;2. 支付记录不可物理删除,只能追加冲正记录;3. 完整的操作审计日志;4. 数据库变更通过 DBA 审核。

Q:Payment Token 和 Order ID 有什么区别?

Order ID 标识一笔业务订单(如购买商品),一个订单可能有多次支付尝试(第一次卡余额不足,换卡再付)。Payment Token 标识一次具体的支付请求,保证这次请求幂等。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页