Arch Day 110
Arch Day 110: 系统设计面试(1) — 设计支付系统
Arch Day 110: 系统设计面试(1) — 设计支付系统
2026-03-29
第四阶段 - 高阶融合系统设计支付系统幂等一致性对账通道路由
日期: 2026-03-29 (Day 110) 阶段: 第四阶段 - 高阶融合 标签: #系统设计 #支付系统 #幂等 #一致性 #对账 #通道路由
面试模拟 — 45分钟
面试场景
面试官: "请设计一个支付系统,支持用户在线购物付款。我们需要支持多种支付方式(信用卡、借记卡、第三方支付),日均交易量从百万级起步,未来要支撑到十亿级。"
第一步:需求澄清(5分钟)
我的提问
功能需求确认:
Q1: 支持哪些支付场景?只有在线支付,还是也包括线下POS?
A: 主要在线支付,后续扩展线下。
Q2: 支付方式包括哪些?
A: 信用卡/借记卡、支付宝/微信支付、银行转账。
Q3: 是否需要退款功能?
A: 是的,需要支持全额和部分退款。
Q4: 是否需要国际支付/多币种?
A: 第一阶段国内,后续支持跨境。
Q5: 对账需求?
A: 需要与支付服务商(PSP)对账,日终结算。
非功能需求确认:
Q6: 延迟要求?
A: 支付发起到结果返回 <3秒。
Q7: 可用性要求?
A: 99.99%(支付系统不能挂)。
Q8: 日均交易量?
A: 当前100万笔/天,峰值500万笔/天。目标支撑10亿笔/天。
Q9: 安全合规要求?
A: PCI-DSS合规,敏感数据加密存储。
需求总结
核心功能:
├── 收单(Acquire): 用户发起支付→选择支付方式→完成支付
├── 退款(Refund): 全额/部分退款
├── 查询(Query): 支付状态查询
├── 对账(Reconcile): 与PSP日终对账
└── 通道路由(Route): 智能选择最优支付通道
非功能要求:
├── 延迟: <3秒(端到端)
├── 可用性: 99.99%
├── 一致性: 资金操作必须强一致
├── 幂等: 网络超时重试不能重复扣款
├── 安全: PCI-DSS合规
└── 扩展: 100万→10亿 笔/天
第二步:高层设计(10分钟)
2.1 架构总览
支付系统高层架构:
┌──────────────────────────────────────────────────────────────┐
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────────┐ │
│ │ 商户/App │───→│ API │───→│ Payment Service │ │
│ │ │ │ Gateway │ │ (支付核心服务) │ │
│ └──────────┘ └──────────┘ └──────────┬───────────┘ │
│ │ │
│ ┌─────────────────────────┼──────┐ │
│ │ │ │ │
│ ┌─────┴─────┐ ┌────────┐ ┌───┴───┐ │ │
│ │ Route │ │ Risk │ │ Ledger│ │ │
│ │ Engine │ │ Engine │ │ (账本) │ │ │
│ │ (通道路由) │ │ (风控) │ │ │ │ │
│ └─────┬─────┘ └────────┘ └───────┘ │ │
│ │ │ │
│ ┌─────┴──────────────────────────────┐ │ │
│ │ Channel Adapter Layer │ │ │
│ │ ┌──────┐ ┌──────┐ ┌──────┐ │ │ │
│ │ │银联 │ │支付宝│ │微信 │ ... │ │ │
│ │ │Adapter│ │Adapter│ │Adapter│ │ │ │
│ │ └──────┘ └──────┘ └──────┘ │ │ │
│ └────────────────────────────────────┘ │ │
│ │ │
│ ┌──────────────────────────────────────┘ │
│ │ │
│ ┌─────┴─────┐ ┌──────────┐ ┌──────────────────┐ │
│ │ Reconcile │ │ Settle │ │ Notification │ │
│ │ Service │ │ Service │ │ Service │ │
│ │ (对账) │ │ (结算) │ │ (通知) │ │
│ └───────────┘ └──────────┘ └──────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
2.2 核心流程
支付核心流程(Happy Path):
用户 商户App API GW Payment Risk Route Channel PSP
│ │ │ Service Engine Engine Adapter │
│─下单──→│ │ │ │ │ │ │
│ │──支付──→│ │ │ │ │ │
│ │ │──创建───→│ │ │ │ │
│ │ │ │──风控──→│ │ │ │
│ │ │ │←─通过──│ │ │ │
│ │ │ │──路由─────────→│ │ │
│ │ │ │←─选择通道────│ │ │
│ │ │ │──调用通道──────────────→│ │
│ │ │ │ │──请求──→│
│ │ │ │ │←─结果──│
│ │ │ │←──通道结果─────────────│ │
│ │ │ │──更新状态 │ │
│ │ │ │──记账────→Ledger │ │
│ │ │←─结果───│ │ │
│ │←─结果──│ │ │ │
│←─结果─│ │ │ │ │
状态机:
CREATED → PROCESSING → SUCCESS / FAILED / TIMEOUT
↓
REFUNDING → REFUNDED / REFUND_FAILED
第三步:核心组件设计(15分钟)
3.1 数据模型
-- 支付订单表(核心)
CREATE TABLE payment_order (
id BIGINT PRIMARY KEY,
order_no VARCHAR(64) UNIQUE NOT NULL, -- 业务订单号
pay_no VARCHAR(64) UNIQUE NOT NULL, -- 支付流水号
merchant_id BIGINT NOT NULL,
amount BIGINT NOT NULL, -- 金额(分)
currency VARCHAR(3) DEFAULT 'CNY',
status VARCHAR(20) NOT NULL, -- CREATED/PROCESSING/SUCCESS/FAILED
pay_method VARCHAR(20), -- CARD/ALIPAY/WECHAT
channel_code VARCHAR(20), -- 实际使用的通道
channel_txn_no VARCHAR(128), -- 通道交易号
idempotency_key VARCHAR(128) UNIQUE, -- 幂等键
expire_time TIMESTAMP, -- 支付超时时间
success_time TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
version INT DEFAULT 0 -- 乐观锁
);
-- 账本表(复式记账)
CREATE TABLE ledger_entry (
id BIGINT PRIMARY KEY,
pay_no VARCHAR(64) NOT NULL,
account_no VARCHAR(64) NOT NULL, -- 账户号
direction VARCHAR(1) NOT NULL, -- D(借)/C(贷)
amount BIGINT NOT NULL, -- 金额(分)
balance_after BIGINT NOT NULL, -- 记账后余额
entry_type VARCHAR(20) NOT NULL, -- PAYMENT/REFUND/FEE
created_at TIMESTAMP DEFAULT NOW()
);
-- 约束:每笔交易的借方总额 = 贷方总额(复式记账平衡)
-- 通道配置表
CREATE TABLE channel_config (
id BIGINT PRIMARY KEY,
channel_code VARCHAR(20) UNIQUE NOT NULL,
channel_name VARCHAR(64),
status VARCHAR(10), -- ACTIVE/INACTIVE
fee_rate DECIMAL(6,4), -- 手续费率
daily_limit BIGINT, -- 日限额
priority INT, -- 优先级
weight INT DEFAULT 100, -- 路由权重
success_rate DECIMAL(5,2), -- 历史成功率
avg_latency_ms INT, -- 平均延迟
updated_at TIMESTAMP
);
-- 对账记录表
CREATE TABLE reconcile_record (
id BIGINT PRIMARY KEY,
recon_date DATE NOT NULL,
channel_code VARCHAR(20) NOT NULL,
our_count INT, -- 我方交易数
our_amount BIGINT, -- 我方金额
their_count INT, -- 对方交易数
their_amount BIGINT, -- 对方金额
diff_count INT, -- 差异数
diff_amount BIGINT, -- 差异金额
status VARCHAR(20), -- MATCHED/DIFF/PENDING
created_at TIMESTAMP DEFAULT NOW()
);
3.2 API设计
核心API:
1. 创建支付
POST /api/v1/payments
Headers:
Idempotency-Key: {unique-key} ★ 幂等键
X-Merchant-Id: {merchant-id}
Authorization: Bearer {token}
Body:
{
"order_no": "ORD-2026032900001",
"amount": 9900, // 分
"currency": "CNY",
"pay_method": "ALIPAY",
"return_url": "https://...",
"notify_url": "https://...", // 异步通知地址
"description": "iPhone 16 Pro",
"expire_seconds": 900 // 15分钟超时
}
Response:
{
"pay_no": "PAY-20260329-XXXXX",
"status": "CREATED",
"pay_url": "https://...", // 跳转支付页面URL
"expire_time": "2026-03-29T15:15:00Z"
}
2. 查询支付
GET /api/v1/payments/{pay_no}
Response:
{
"pay_no": "PAY-20260329-XXXXX",
"status": "SUCCESS",
"amount": 9900,
"success_time": "2026-03-29T15:01:23Z",
"channel_txn_no": "CH-20260329-YYYYY"
}
3. 退款
POST /api/v1/payments/{pay_no}/refunds
Headers:
Idempotency-Key: {unique-key}
Body:
{
"refund_amount": 9900,
"reason": "用户申请退款"
}
4. 异步通知(Webhook)
POST {merchant_notify_url}
Body:
{
"pay_no": "PAY-20260329-XXXXX",
"order_no": "ORD-2026032900001",
"status": "SUCCESS",
"amount": 9900,
"sign": "SHA256签名" // 防篡改
}
// 商户返回 "SUCCESS" 表示收到
// 未收到SUCCESS → 重试(1s, 5s, 30s, 5m, 30m, 1h, 6h, 24h)
3.3 幂等性设计 ★ 面试重点
幂等性实现方案:
┌──────────────────────────────────────────────────────────┐
│ 幂等性处理流程 │
├──────────────────────────────────────────────────────────┤
│ │
│ 请求进入 │
│ │ │
│ ├── 1. 提取 Idempotency-Key │
│ │ (由调用方生成,全局唯一) │
│ │ │
│ ├── 2. 查询幂等表 │
│ │ SELECT * FROM idempotent_record │
│ │ WHERE idempotency_key = ? │
│ │ │
│ ├── 3a. 记录不存在 → 新请求 │
│ │ INSERT INTO idempotent_record │
│ │ (key, request_hash, status='PROCESSING') │
│ │ → 执行业务逻辑 │
│ │ → 更新 status='DONE', response=结果 │
│ │ │
│ ├── 3b. 记录存在 + status='DONE' → 重复请求 │
│ │ → 直接返回上次的 response(不重复执行) │
│ │ │
│ └── 3c. 记录存在 + status='PROCESSING' → 并发请求 │
│ → 等待或返回"处理中" │
│ │
│ 关键设计点: │
│ ├── Idempotency-Key 由调用方生成(通常是 订单号+操作类型)│
│ ├── 幂等记录和业务操作在同一个事务中 │
│ ├── 幂等记录设置TTL(例如24小时),自动清理 │
│ └── 请求Hash校验:相同Key但不同请求参数 → 返回错误 │
└──────────────────────────────────────────────────────────┘
3.4 通道路由设计
通道路由引擎:
┌──────────────────────────────────────────────────────────┐
│ Route Engine │
├──────────────────────────────────────────────────────────┤
│ │
│ 输入:支付请求(金额/支付方式/商户/用户) │
│ │
│ Step 1: 可用通道筛选 │
│ ├── 通道状态 = ACTIVE │
│ ├── 支持该支付方式 │
│ ├── 日限额未满 │
│ └── 金额在通道限额范围内 │
│ │
│ Step 2: 通道评分 │
│ Score = w1 × 成功率 │
│ + w2 × (1/延迟) │
│ + w3 × (1/费率) │
│ + w4 × 优先级 │
│ │
│ 权重示例: │
│ w1=0.4(成功率最重要) │
│ w2=0.2(延迟) │
│ w3=0.3(费率) │
│ w4=0.1(优先级) │
│ │
│ Step 3: 选择通道 │
│ ├── 正常模式: 加权随机选择(按评分加权) │
│ └── 降级模式: 按优先级顺序选择 │
│ │
│ Step 4: 失败重试 │
│ ├── 通道A失败 → 自动切换通道B │
│ ├── 最多重试2次 │
│ └── 每次切换选择下一个最优通道 │
│ │
│ 通道健康度实时更新: │
│ ├── 滑动窗口统计(最近5分钟成功率) │
│ ├── 成功率<80% → 降权 │
│ ├── 成功率<50% → 熔断(暂停使用该通道) │
│ └── 恢复检测: 每30秒发一笔探测交易 │
└──────────────────────────────────────────────────────────┘
3.5 对账系统设计
对账流程(每日凌晨执行):
┌──────────────────────────────────────────────────────────┐
│ Daily Reconciliation │
├──────────────────────────────────────────────────────────┤
│ │
│ Step 1: 获取数据 │
│ ├── 我方: 从payment_order表查询昨日所有交易 │
│ └── 对方: 下载PSP提供的对账文件(通常是CSV/Excel) │
│ │
│ Step 2: 逐笔比对 │
│ ├── 以我方流水号和对方交易号进行匹配 │
│ ├── 匹配成功 → 比对金额/状态 │
│ │ ├── 完全一致 → 标记 MATCHED │
│ │ └── 不一致 → 标记 DIFF + 记录差异详情 │
│ └── 未匹配: │
│ ├── 我方有对方无(长款) → 可能对方延迟结算 │
│ └── 对方有我方无(短款) → 可能我方掉单 ⚠️ │
│ │
│ Step 3: 差异处理 │
│ ├── 自动处理: │
│ │ ├── 金额差≤1分 → 自动调平(四舍五入导致) │
│ │ └── 状态差异+我方pending → 主动查询PSP同步状态 │
│ └── 人工处理: │
│ ├── 金额差>1分 → 人工审核 │
│ ├── 短款 → 紧急排查 │
│ └── 连续3天未匹配 → 升级处理 │
│ │
│ Step 4: 生成报告 │
│ ├── 对账总览(匹配率/差异笔数/差异金额) │
│ ├── 差异明细(每笔差异的详情) │
│ └── 告警通知(差异率>0.1%告警) │
└──────────────────────────────────────────────────────────┘
第四步:深入讨论(10分钟)
4.1 一致性保证
支付系统的一致性挑战:
问题: 内部扣款成功 但 调用PSP超时 → 不知道是否真的扣款了
解决方案:
方案1: 本地消息表(Transactional Outbox)
┌──────────────────────────────────────────┐
│ Transaction: │
│ BEGIN │
│ UPDATE account SET balance -= 100 │
│ INSERT INTO outbox (msg_type='PAY', │
│ payload={...}, status='PENDING') │
│ COMMIT │
│ │
│ 异步: │
│ 定时任务扫描 outbox 表 │
│ → 发送到PSP │
│ → 成功: status='SENT' │
│ → 失败: 重试(最多N次) │
│ → 超时: 主动查询PSP状态 │
└──────────────────────────────────────────┘
方案2: 状态机 + 补偿
┌──────────────────────────────────────────┐
│ 正向流程: │
│ CREATED → PROCESSING → SUCCESS │
│ │
│ 超时处理: │
│ PROCESSING超过30秒 → 主动查询PSP │
│ ├── PSP说成功 → 更新为SUCCESS │
│ ├── PSP说失败 → 更新为FAILED + 退款 │
│ └── PSP也不知道 → 标记UNCERTAIN │
│ → 等对账时确认 │
│ │
│ 补偿流程(最终一致): │
│ UNCERTAIN → 对账确认 → SUCCESS/退款 │
└──────────────────────────────────────────┘
关键原则:
├── "宁可多查不可少查" — 超时一定要主动查询PSP
├── "宁可退款不可多扣" — 不确定时倾向于退款给用户
└── "对账是安全网" — 所有不一致最终通过对账发现
4.2 安全设计
支付安全要点:
1. PCI-DSS合规
├── 卡号不落库 → 使用Token化服务(替代卡号)
├── 传输加密 → 全链路TLS 1.3
├── 存储加密 → AES-256加密敏感字段
└── 访问控制 → 最小权限 + 审计日志
2. 防重放攻击
├── Idempotency-Key(已有)
├── 请求时间戳校验(±5分钟)
├── 签名验证(HMAC-SHA256)
└── Nonce(一次性随机数)
3. 防篡改
├── 请求签名: 商户用私钥签名,系统用公钥验证
├── 通知签名: 系统用私钥签名,商户用公钥验证
└── 关键字段(金额/收款方)参与签名
4. 限流与反欺诈
├── IP级限流: 单IP 100次/分钟
├── 商户级限流: 根据合同配置
├── 异常检测: 短时间大量小额交易→告警
└── 黑名单: 已知欺诈IP/商户/卡号
第五步:扩展性设计(5分钟)
5.1 从百万到十亿级
扩展策略:
━━━━━ 阶段1: 百万级(100万笔/天) ━━━━━
├── 单机房部署
├── 主从数据库(MySQL)
├── Redis缓存热点数据
├── 单Redis做分布式锁
└── 日处理量: ~12 TPS均值, ~100 TPS峰值
━━━━━ 阶段2: 千万级(1000万笔/天) ━━━━━
├── 数据库分库分表
│ ├── 按商户ID分库(8库)
│ └── 按时间分表(月表)
├── 读写分离
├── 消息队列(Kafka)异步化
│ ├── 通知异步发送
│ └── 对账数据异步写入
├── 多活部署(同城双中心)
└── 日处理量: ~120 TPS均值, ~1000 TPS峰值
━━━━━ 阶段3: 亿级(1亿笔/天) ━━━━━
├── 微服务拆分(支付/风控/路由/对账/结算独立服务)
├── 数据库分片(64+分片)
├── 热点账户分桶
│ └── 热门商户账户拆成N个子账户,汇总时合并
├── 异地多活(双城三中心)
├── 去中心化ID生成(Snowflake)
└── 日处理量: ~1200 TPS均值, ~10000 TPS峰值
━━━━━ 阶段4: 十亿级(10亿笔/天) ━━━━━
├── 全球化多Region部署
├── 流式计算(Flink)替代批处理对账
├── 时序数据库存储交易日志
├── CQRS(读写分离架构)
│ ├── 写: 事件溯源(Event Sourcing)
│ └── 读: 物化视图 + ES搜索
├── 自动化扩缩容(K8s HPA)
└── 日处理量: ~12000 TPS均值, ~100000 TPS峰值
5.2 高可用设计
高可用方案:
┌──────────────────────────────────────────────────────────┐
│ 高可用设计要点 │
├──────────────────────────────────────────────────────────┤
│ │
│ 1. 服务层高可用 │
│ ├── 无状态服务: 多实例+负载均衡 │
│ ├── 优雅关闭: 处理完当前请求再关闭 │
│ ├── 健康检查: /health + /ready │
│ └── 超时控制: 每个外部调用设置超时 │
│ │
│ 2. 数据层高可用 │
│ ├── 主从复制: 异步复制(0.5-1秒延迟) │
│ ├── 自动故障转移: MHA/Orchestrator │
│ ├── 跨机房备份: 每日全量+实时Binlog同步 │
│ └── 分库分表: 单库故障只影响部分商户 │
│ │
│ 3. 通道层高可用 │
│ ├── 多通道冗余: 同一支付方式至少2个通道 │
│ ├── 自动切换: 通道A故障→自动切换通道B │
│ ├── 熔断器: 成功率<50%→暂停该通道 │
│ └── 降级策略: 所有通道不可用→引导用户使用其他支付方式 │
│ │
│ 4. 容灾策略 │
│ ├── 同城双中心: 两个机房Active-Active │
│ ├── 异地灾备: RPO<30秒, RTO<5分钟 │
│ └── 定期演练: 每季度一次故障切换演练 │
│ │
└──────────────────────────────────────────────────────────┘
面试官追问及应对
追问1: "如果PSP返回超时,你怎么处理?"
PSP超时处理策略:
1. 不能假设失败→立即退款(PSP可能已经扣款成功)
2. 不能假设成功→给用户确认(可能真的失败了)
正确做法:
├── Step 1: 标记为UNCERTAIN状态
├── Step 2: 立即(1-3秒后)主动查询PSP状态
│ ├── PSP返回成功 → 更新为SUCCESS
│ ├── PSP返回失败 → 更新为FAILED
│ └── PSP也不确定 → 继续Step 3
├── Step 3: 启动定时重试查询(5s, 30s, 5m, 30m)
│ └── 最多查询6次(约1小时)
├── Step 4: 仍不确定 → 等待日终对账确认
│ ├── 对账发现已扣款 → 补更新为SUCCESS
│ └── 对账发现未扣款 → 更新为FAILED
└── Step 5: 对用户展示
├── 立即返回"支付处理中"
├── 确认成功后通知用户
└── 确认失败后引导重新支付
追问2: "如何保证不重复扣款?"
三道防线:
第一道: Idempotency-Key
├── 相同Key的请求只处理一次
└── Key = 订单号 + 操作类型(如 ORD-001-PAY)
第二道: 状态机控制
├── CREATED → PROCESSING(只允许一次状态转移)
├── 使用乐观锁(version字段)防止并发
└── UPDATE ... WHERE status='CREATED' AND version=N
第三道: PSP侧幂等
├── 传递相同的商户订单号给PSP
├── PSP自身会做去重
└── 同一商户订单号不会扣两次款
三道防线任意一道起作用就能防止重复扣款。
追问3: "如何处理跨境支付?"
跨境支付扩展:
新增组件:
├── 汇率服务: 实时获取+定期刷新+锁定机制
├── 合规服务: 制裁名单检查/大额申报/税务
├── 多币种账本: 支持多币种记账+汇兑损益
└── 国际通道: VISA/MasterCard/SWIFT
关键挑战:
├── 汇率波动: 从用户下单到实际扣款可能有汇率变动
│ → 汇率锁定(15分钟内有效)
├── 结算时间: 跨境结算T+2到T+5
│ → 对账周期相应延长
├── 合规要求: 不同国家不同要求
│ → 合规规则引擎(可配置)
└── 时区差异: 日切时间不同
→ 统一使用UTC时间
架构决策记录(ADR)
ADR-1: 选择本地消息表而非分布式事务
决策: 使用本地消息表(Transactional Outbox)保证一致性
替代方案: TCC分布式事务 / Saga
原因:
├── 本地消息表实现简单,可靠性高
├── TCC对PSP有侵入——PSP不支持Try/Confirm/Cancel
├── Saga复杂度高,回滚逻辑容易出错
└── 支付系统的最终一致性可以接受(有对账兜底)
ADR-2: 选择幂等键由调用方生成
决策: Idempotency-Key由商户/调用方生成
替代方案: 服务端生成
原因:
├── 调用方知道"什么是重复"——同一笔订单支付就是重复
├── 服务端生成需要额外Round Trip
├── 行业惯例(Stripe/PayPal都是调用方生成)
└── 简化服务端逻辑
今日总结
支付系统设计核心要点
- 幂等性是支付系统第一原则——网络超时必须可安全重试
- 状态机+补偿解决分布式一致性——最终一致而非强一致
- 对账是安全网——所有不一致最终通过对账发现和修复
- 通道路由是成本优化的关键——按成功率/费率/延迟动态选择
- 安全是底线——PCI-DSS、签名验证、防重放、Token化
- 扩展性分阶段规划——不要过早优化,但要预留扩展点
面试答题框架
45分钟分配:
5min 需求澄清(问清楚再动手)
10min 高层设计(先画全景图)
15min 核心组件(数据模型+API+关键流程)
10min 深入讨论(一致性/安全/幂等)
5min 扩展性(容量规划+高可用)