设计一个支付系统

本文系统设计一个高可用、高一致性的支付平台:覆盖支付网关与渠道适配、订单到结算再到对账的完整链路、幂等与状态机、双写一致性与最终一致、分布式事务方案,以及风控限额与差错处理,并给出数据表结构与量级估算。

支付系统是几乎所有互联网商业系统的「收款底座」,同时也是对一致性、可用性和资金安全要求最苛刻的系统之一:钱不能多扣、不能少扣、不能扣了不通知、更不能对不上账。本文按照系统设计面试的标准答题结构,设计一个支持多渠道、多币种、每日数千万笔交易的支付平台。

一句话:支付系统的核心不是「转账」,而是「记账 + 对账」——用幂等、状态机和最终一致把每一笔钱的状态收敛到确定值。

一、需求澄清与量级估算

1.1 需求澄清

面试官给出题目「设计一个支付系统」后,先通过提问明确边界:

  • 支付场景:只做线上支付(App / Web / 小程序下单扣款),还是包含转账、提现、退款、代扣、扫码线下?
  • 渠道:接入哪些渠道?支付宝、微信、银联、银行卡直连,还是仅内部钱包账户?
  • 币种:单币种(人民币)还是多币种(跨境支付)?
  • 资金流向:是否需要两级账户(平台商户账户 + 用户账户)?
  • 合规:是否需要风控、反洗钱、差错处理、T+1 对账?

明确假设(面向面试的合理假设):

需求项假设
支付方式余额钱包、银行卡快捷、支付宝/微信三方代收
货币人民币,预留多币种字段
商户电商平台自营 + 外部商户入驻(抽佣)
结算T+1 自动结算到商户银行账户,支持手动提现
可靠性支付成功率 99.99% 以上、资金零差错(对账兜底)

1.2 量级估算

指标估算值推导
日支付订单1000 万笔假设日活 5000 万用户,20% 当日下单
峰值 QPS约 5000日均 1000 万 / 86400 ≈ 116,再乘 40 倍峰值系数
每秒峰值扣款~5000 笔与支付 QPS 同量级
年交易额~1.2 万亿客单价 300 元 × 日均 1000 万 × 365
对账单行数数千万/天每笔支付至少产生 1 支付单 + 1 结算单 + N 个流水

一句话:单机 MySQL 扛不住千万级日单量,必须分库分表 + 异步化 + 削峰,同时保证「扣钱」这条链路的强一致。

二、高层架构设计

                    ┌────────────────────────────────────────────┐
                    │                  客户端 (App / Web / H5)     │
                    └──────────────────────┬─────────────────────┘
                                           │ 下单+支付请求
                    ┌──────────────────────▼─────────────────────┐
                    │               BFF / 业务网关                 │
                    │        (鉴权、签名校验、参数规整、限流)         │
                    └──────────────────────┬─────────────────────┘
                                           │
              ┌────────────────────────────▼────────────────────────────┐
              │                       支付核心服务                          │
              │   ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐ │
              │   │ 订单服务  │  │支付引擎   │  │结算服务   │  │对账服务   │ │
              │   └──────────┘  └──────────┘  └──────────┘  └──────────┘ │
              │   ┌──────────┐  ┌──────────┐  ┌──────────┐               │
              │   │ 账户服务  │  │账务流水   │  │风控服务   │               │
              │   └──────────┘  └──────────┘  └──────────┘               │
              └──────────┬────────────────────────────────────────────────┘
                         │ 通过 MQ / RPC
              ┌──────────▼──────────────────────────────────────────┐
              │                支付网关(渠道网关层)                    │
              │   支付宝适配器   微信适配器   银联适配器   银行卡直连      │
              │   (协议转换 / 签名验签 / 加密 / 回调接收)                 │
              └──────────┬──────────────────────────────────────────┘
                         │
        ┌────────────────▼─────────────────┐
        │        第三方渠道 / 银联 / 网联      │
        │        (真正发生资金流动的地方)       │
        └──────────────────────────────────┘

整体拆为四层:

  1. 接入层:业务网关,负责鉴权、验签、限流。
  2. 核心服务层:订单、支付引擎、账户、结算、对账、风控六个领域服务。
  3. 渠道层:支付网关 + 各渠道适配器,屏蔽不同渠道协议差异。
  4. 依赖设施:MySQL(分库分表)、Redis(幂等/锁/风控计数)、MQ(异步解耦)、时序/日志(审计)。

2.1 为什么拆成六个领域服务

  • 订单服务:只负责业务订单状态,不碰资金。
  • 支付引擎:发起到渠道的支付,维护支付单状态机。
  • 账户服务:管用户/商户余额与流水,记账必须幂等。
  • 结算服务:T+1 汇总商户应收,生成结算单。
  • 对账服务:拉取渠道账单与本地流水核对,发现差错并挂账。
  • 风控服务:在支付前/中/后拦截风险交易。

一句话:按「单、账、钱、账务核对」分离,让资金链路可审计、可回溯;任何一步出问题都有对账兜底,不会悄悄出错。

三、核心组件设计

3.1 支付引擎与支付单

支付引擎是核心,它接收「支付请求」,生成支付单(payment order),然后驱动一次支付的生命周期:

CREATE TABLE payment_order (
  payment_id      BIGINT        COMMENT '支付单号,雪花ID',
  biz_order_id    VARCHAR(64)   COMMENT '业务订单号',
  user_id         BIGINT,
  merchant_id     BIGINT,
  channel         VARCHAR(32)   COMMENT '渠道:ALIPAY/WECHAT/CARD/BALANCE',
  amount_cents    BIGINT        COMMENT '金额,单位分',
  currency        VARCHAR(8)    DEFAULT 'CNY',
  status          TINYINT       COMMENT '0初始 1支付中 2成功 3失败 4已退款 5关闭',
  scene           VARCHAR(32)   COMMENT 'APP/WEB/SCAN',
  risk_checked    TINYINT,
  channel_trade_no VARCHAR(64)  COMMENT '渠道侧交易号',
  callback_count  INT           DEFAULT 0,
  created_at      DATETIME,
  updated_at      DATETIME,
  PRIMARY KEY (payment_id),
  KEY idx_user (user_id, created_at),
  KEY idx_channel_trade_no (channel, channel_trade_no),
  UNIQUE KEY uk_biz_order (biz_order_id)  -- 关键:一笔业务订单只对应一个支付单
) COMMENT='支付单';

状态机(不允许非法跳转):

    ┌────────────┐   创建    ┌────────────┐   发起    ┌────────────┐
    │  PAY_INIT  │──────────▶│ PAY_PROCESS │─────────▶│ PAY_PENDING │──┐
    └────────────┘           └────────────┘           └────────────┘  │
                                                                     ▼
                                      ┌──────────────────────────────┐
         ┌────────────────────────────┤   PAY_SUCCESS (终态,入账)      │
         │      渠道回调/主动查询        └──────────────────────────────┘
         │                             ┌──────────────────────────────┐
         └────────────────────────────▶│   PAY_FAIL / PAY_CLOSED       │
             超时/用户取消              └──────────────────────────────┘

一句话:支付单是一切的锚点,状态机 + 唯一索引保证一笔订单只被支付一次,重复回调、重复查询都收敛到同一状态。

3.2 支付网关与渠道适配

渠道适配器模式:统一内部接口,各渠道实现差异逻辑。

class ChannelAdapter(ABC):
    def pay(self, req: PayRequest) -> PayResponse: ...
    def query(self, trade_no: str) -> QueryResponse: ...
    def refund(self, req: RefundRequest) -> RefundResponse: ...
    def parse_callback(self, raw: dict) -> CallbackEvent: ...

class AlipayAdapter(ChannelAdapter):
    def pay(self, req):
        # 拼装支付宝请求报文 -> 加签 -> HTTP 调用 -> 解析结果
        ...

支付网关三件职责:

  1. 协议转换:内部统一结构 ↔ 渠道报文。
  2. 安全:请求加签、响应验签、证书管理、敏感信息(卡号)脱敏。
  3. 回调接收:一个统一 /callback/{channel} 端点,验签后写入 MQ 给支付引擎消费(防重放:渠道侧交易号唯一键)。

3.3 账务流水与记账

记账采用「双分录(复式记账)」:每笔资金变动同时记借方与贷方,总额守恒。账务流水表:

CREATE TABLE account_transaction (
  txn_id       BIGINT PRIMARY KEY,
  account_id   BIGINT,          -- 账户ID
  user_id      BIGINT,
  direction    TINYINT,         -- 1 加钱  2 减钱
  amount_cents BIGINT,
  balance_before BIGINT,
  balance_after  BIGINT,
  biz_type     VARCHAR(32),     -- PAYMENT / REFUND / SETTLEMENT / WITHDRAW
  ref_id       VARCHAR(64),     -- 关联支付单号,全局唯一 -> 幂等
  status       TINYINT,
  created_at   DATETIME,
  UNIQUE KEY uk_ref (biz_type, ref_id, direction)  -- 幂等核心
) COMMENT='账户流水';

记账逻辑伪代码:

def post_entries(account_book, txn_id, debit, credit, ref_id):
    with db.transaction():
        # 1. 幂等校验:同一 ref_id + 类型 已记账则直接返回成功
        if exists(AccountTransaction, ref_id=ref_id, txn_type="PAYMENT"):
            return OK
        # 2. 乐观锁更新余额(扣减方)
        rows = db.update(
            "UPDATE account SET balance = balance - :amt, version = version+1 "
            "WHERE id = :debit AND balance >= :amt AND version = :expect"
        )
        if rows == 0:
            raise InsufficientBalance
        # 3. 写流水(同事务)
        db.insert(AccountTransaction(debit...)); db.insert(AccountTransaction(credit...))
        # 4. commit

3.4 风控与限额

风控必须与支付主链路解耦,否则一次风控超时就会拖垮整个支付:

风控规则引擎(离线规则 + 在线实时决策)
  ┌──────────────┐   ┌──────────────┐   ┌──────────────┐
  │ 设备指纹      │   │ 黑名单/灰名单 │   │ 规则集(命中打分) │
  └──────────────┘   └──────────────┘   └──────────────┘
  入参: 用户/设备/金额/频次/IP/商户 → 决策: 放行 / 增强验证 / 拒绝 / 人工审核

常用风控维度:

维度例子实现
频次限额单用户单日支付笔数上限、单卡单日限额Redis 计数 + 阈值
金额限额单笔上限、日累计上限、单商户集中度限额表 + 实时累加
行为异常短时间异地登录、设备突变、刷单特征设备指纹 + 规则
黑名单欺诈用户、风险商户、风险卡 BIN布隆过滤器 / 名单表

一句话:风控是「异步规则 + 同步最小决策」——能异步判定的绝不同步阻塞,必须同步拦截的高危交易才进主链路。

3.5 退款与差错处理

退款是支付里最容易「多退、重复退」的地方,设计要点:

  • 退款单:以 refund_order 为锚点,ref_id = payment_id 唯一约束,同一支付单的累计退款金额不得超过原支付金额(用事务 + 乐观锁校验 paid_amount - refunded_amount >= refund_amount)。
  • 原路退回:渠道退款接口,银行卡/钱包原路返回;退款也是异步流程,同样走回调 + 幂等。
  • 差错处理:对账发现「我方成功渠道失败」「渠道成功我方未记账」「金额不一致」等情况,进入差错池挂账,由人工/规则引擎逐笔处理,绝不允许静默覆盖。

四、数据模型

核心表族:

表用途分片键说明
payment_order支付单user_id一笔订单一张支付单
account账户余额user_id/merchant_id资金主体
account_transaction账务流水account_id双分录,可对账
settle_order结算单merchant_idT+1 汇总
refund_order退款单payment_id原路退回
risk_event风控事件user_id规则命中记录
channel_bill渠道对账单channel + date拉取的文件/明细

字段规范:金额一律用 BIGINT(单位分),严禁浮点;时间用 DATETIME 统一 +08:00;所有变更走状态机,不做物理删除。

五、关键流程

5.1 一次「余额支付」的时序

App            BFF          支付引擎        账户服务        支付单        Redis/DB
 │ 下单支付请求   │              │              │              │
 ├─────────────▶│ 校验+鉴权      │              │              │
 │              ├─────────────▶│ 创建支付单     │              ├── 插入支付单(INIT)
 │              │              ├─────────────▶│ 预冻结余额     │
 │              │              │              ├─────────────▶│ 锁+扣减余额
 │              │              │              │              │ 写流水(幂等)
 │              │              │◀─────────────┤ 冻结成功       │
 │              │              ├─────────────▶│ 确认支付(记账)  │
 │              │              │              ├─────────────▶│ 状态→SUCCESS
 │              │◀─────────────┤ 成功          │              │
 │◀─────────────┤              │              │              │
 │  返回支付结果   │              │              │              │

关键点:余额扣减与写流水在同一数据库事务内完成;「预冻结 → 确认」两步让支付与后续退款、取消都有明确锚点。

5.2 渠道支付回调(异步)

渠道 → 支付网关(验签) → MQ(callback_topic) → 支付引擎(幂等消费)
       支付引擎: 更新支付单 SUCCESS → 记账(用户钱包入账/平台应收+)
       失败重试: 按指数退避重试,超过 N 次转人工差错池

一句话:渠道回调是异步的,绝不能同步依赖;所有写操作都以「查询支付单当前状态」为准,天然防重放。

六、可靠性与一致性

6.1 双写一致性与最终一致

支付引擎在更新支付单状态后,需要「同时」更新订单服务状态并通知下游。为了避免分布式双写不一致,采用「本地消息表 / 事务消息 + 对账兜底」:

  1. 支付引擎在本地事务里 update payment_order + insert outbox(message)。
  2. 后台任务扫描 outbox,把消息可靠投递到 MQ。
  3. 下游(订单服务、结算服务、通知)消费后幂等落地。
  4. 定时任务补偿:扫描「支付单成功但订单未同步」的孤儿,重新投递。

6.2 分布式事务方案对比

方案一致性吞吐适用局限
2PC / XA强一致低极少数强一致场景阻塞、性能差,支付系统基本不用
TCC(Try-Confirm-Cancel)最终一致中需要「预留资源」的扣减场景(余额、库存)侵入业务,实现复杂
本地消息表 + MQ最终一致高订单/支付/结算的异步通知需要消息表 + 补偿
Saga最终一致高长事务编排(下单→支付→发货)需补偿幂等

支付链路最常用组合:余额扣减用 TCC(预冻结/确认/解冻)+ 状态通知用本地消息表;不做跨库 2PC。

6.3 幂等设计清单

  • 支付单:biz_order_id 唯一约束。
  • 账务流水:(biz_type, ref_id, direction) 唯一约束。
  • 渠道回调:(channel, channel_trade_no) 唯一键 + 状态机幂等。
  • 退款单:payment_id 只能对应有限次退款,退款金额累加不得超过原单。

6.4 对账流程(T+1)

对账是资金安全的「最后一道防线」,即使前面的幂等、事务都做对了,仍可能因为渠道故障、人为 bug 产生差错,必须靠对账兜底:

每日凌晨: 任务调度器触发对账
  ├─ 拉取各渠道账单(文件/FTP/接口) → 落库 channel_bill
  ├─ 按 (渠道, 渠道交易号) 与本地 payment_order 比对
  │    ├─ 两边都有且金额一致 → 对平
  │    ├─ 我方成功/渠道缺失 → 查渠道流水, 可能是掉单, 主动查询确认
  │    └─ 我方失败/渠道成功 → 用户可能已扣款, 触发自动退款或挂账
  ├─ 汇总差异 → 生成对账差异报表 → 差错池分派处理
  └─ 输出对账单确认 → 通知财务/商户

对账要点:

  1. 必须用「渠道侧交易号」比对,不能用我方订单号(渠道侧的号才是资金真实记录)。
  2. 金额单位统一:两边都换算成分比较,避免精度问题。
  3. 对账频率可升级:T+1 是保底,大额交易可加「实时对账」(支付成功即异步核对),小额走日终。

一句话:对账不是「锦上添花」,而是支付系统的「安全带」——所有假设都可能被现实击穿,对账保证最坏情况也能发现并修正。

七、性能与扩展

  • 分库分表:支付单按 user_id 分 128 库 × 32 表;账户按 account_id 哈希分片;结算单按 merchant_id。
  • 读写分离 + 主从延迟容忍:支付查询走从库,资金写入走主库;对账、报表走离线数仓(ClickHouse)。
  • 异步削峰:回调、通知、结算全部 MQ 化,支付高峰不阻塞主链路。
  • 缓存:支付单热状态用 Redis 短暂缓存(TTL 5 分钟),但永远以 DB 为准,缓存只做读加速。
  • 网关超时与熔断:渠道 RPC 超时 3 秒快速失败,连续失败熔断,切备用渠道。

容量与热点

  • 单库单表 QPS 上限约 2000-5000,分库后轻松支撑 10 万级写入。
  • 热点账户(大商户、头部主播)用「账户拆分 + 汇总」或本地缓冲批量记账。
  • 对账文件从渠道拉取用异步任务池,避免占满带宽。

八、权衡与备选

决策点本文选型备选权衡说明
支付单存储MySQL 分库分表TiDB(分布式 NewSQL)MySQL 成熟可控;TiDB 免分片运维,适合扩张快的小团队
消息队列Kafka(事务消息)RocketMQRocketMQ 事务消息开箱即用;Kafka 吞吐更高、生态更广
余额扣减TCC 预冻结单库事务直扣预冻结支持超时自动解冻,但实现复杂;直扣简单但难支持部分冻结
对账引擎自研批处理数仓 + Flink SQL自研灵活可控;Flink 适合实时对账与复杂聚合
多币种金额×汇率快照实时汇率快照汇率保证账目确定;实时汇率账面波动大、难对账

关键取舍

  • 可用性 vs 一致性:支付链路「宁可慢不可错」,先保证 DB 主库一致性,用异步拉平下游;对账 T+1 兜底。
  • 监控 vs 性能:每笔支付写审计日志 + 埋点,成本可接受,但避免在主链路做全文审计。
  • 自研网关 vs 买三方网关:起步用三方(如 Ping++),规模大了自建,降低费率与故障依赖。

九、扩展场景与面试追问

9.1 支持钱包转账

如果题目扩展为「钱包 + 转账」,核心变化:

  • 账户体系:引入「用户钱包账户 + 平台结算账户 + 商户待结算账户」多级账户树。
  • 转账:本质是「扣 A 账户 + 加 B 账户」的原子双写,必须在一个事务/一个分区内完成,或拆为 TCC(Try 冻结 → Confirm 划转 → Cancel 解冻)。
  • 余额对账:定期跑「账户余额 = Σ 流水」校验,发现账实不符立即熔断提现。

9.2 多币种与跨境

  • 增加 currency 与 exchange_rate_snapshot(下单时锁定汇率)。
  • 结算按「原始币种」记账,换汇走独立换汇服务,避免汇率波动影响账面。
  • 合规上需考虑外汇申报、反洗钱,这些是「非功能需求」,提一嘴即可。

9.3 面试常见追问

追问关键回答
支付超时怎么办?主动查询渠道 + 状态机收敛 + 定时补偿,不假设渠道会回调
渠道挂了怎么办?网关熔断 + 自动切换备用渠道 + 失败进入重试队列
用户重复点了支付按钮?前端幂等键 + 后端 biz_order_id 唯一约束,第二次直接返回原支付单
如何保证不超扣?记账用乐观锁 + 余额校验;对账用借贷平衡与渠道流水双核对
分布式事务为什么不用 2PC?2PC 阻塞、性能差、协调者单点,支付链路用 TCC + 消息表 + 对账更务实

十、总结

模块关键设计一句话记忆
支付单状态机 + 唯一业务键一笔订单一个支付单,状态收敛到终态
渠道网关适配器 + 验签 + 回调防重放屏蔽渠道差异,回调必须幂等
账户账务复式记账 + 流水唯一键借贷平衡,流水幂等
一致性本地消息表 + 对账兜底双写靠最终一致,差错靠对账
风控事前规则 + 事中实时拦截 + 事后分析与支付主链路异步解耦
扩展分库分表 + MQ 削峰 + 熔断高吞吐靠水平扩展,别单点扛

一句话:支付系统的面试核心是讲清楚「为什么需要支付单状态机、怎么做幂等、如何用最终一致 + 对账兜底保证资金零差错」,并把量级估算挂在嘴边上,而不是堆组件。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「design」更多文章

  1. 设计一个消息队列系统(类 Kafka)
  2. 设计日志与监控系统
  3. 设计搜索引擎