电商系统设计系列 (篇次与(一)推荐阅读顺序 一致)
引言 支付系统是电商平台的资金流枢纽,连接用户、平台、商家、第三方支付等多方角色。本文从系统设计面试的角度,深入解析支付系统的核心流程、状态机设计、分布式事务等高频考点。
适合读者 :准备系统设计面试的候选人
阅读时长 :30-40 分钟
核心内容 :
支付系统整体架构
支付和退款流程
状态机设计
分布式事务(Saga/TCC)
幂等性设计
一致性保证
一、业务背景与挑战 1.1 支付系统的定位 支付系统是电商平台的资金流枢纽 ,承担以下职责:
C 端 :用户支付、退款、余额管理
B 端 :商家结算、提现、对账
平台 :分账、风控、审计
支付系统与其他系统的协作关系:
graph LR
A[用户] -->|支付| B[支付系统]
B -->|状态同步| C[订单系统]
B -->|优惠计算| D[营销系统]
B -->|扣减库存| E[库存系统]
B -->|触发发货| F[物流系统]
1.2 核心业务场景
场景
说明
关键点
标准支付
用户使用余额、微信、支付宝支付订单
组合支付、渠道路由
退款
全额退款、部分退款、营销优惠退款
可退金额计算、多次退款
清结算
平台佣金、商家收益、营销补贴分账
T+N 结算、提现管理
对账
交易对账、资金对账、差错处理
长款、短款、人工复核
1.3 核心挑战 支付系统面临的核心挑战及对应技术方案:
挑战维度
具体问题
技术方案
资金安全
强一致性、防重防篡改、审计追溯
分布式事务、幂等性、操作日志
高并发
大促期间支付峰值(如双 11)
Redis 缓存、限流、降级、异步化
分布式事务
支付成功后订单状态同步
Saga、TCC、本地消息表
多渠道接入
微信、支付宝、银行等渠道差异
支付网关、策略模式、适配器模式
对账复杂度
多方对账、差错处理
定时任务、对账算法、人工复核
面试重点 :
在系统设计面试中,面试官通常会从以下角度考察:
如何保证支付与订单的最终一致性? → 分布式事务
如何防止用户重复支付? → 幂等性设计
支付系统如何应对高并发? → 缓存、限流、降级
第三方支付回调失败怎么办? → 重试机制、补偿
二、整体架构设计 2.1 分层架构 支付系统采用经典的分层架构,每层职责清晰:
graph TB
subgraph 接入层
A1[C 端小程序/APP]
A2[B 端运营后台]
A3[Open API]
end
subgraph 应用服务层
B1[订单服务]
B2[支付服务]
B3[对账服务]
end
subgraph 核心业务层
C1[账户系统]
C2[支付网关]
C3[清结算引擎]
C4[风控系统]
end
subgraph 基础设施层
D1[MySQL]
D2[Redis]
D3[Kafka]
D4[XXL-Job]
end
subgraph 第三方服务层
E1[微信支付]
E2[支付宝]
E3[银行网关]
end
A1 --> B2
A2 --> B2
A3 --> B2
B1 --> C2
B2 --> C1
B2 --> C2
B2 --> C3
B2 --> C4
C1 --> D1
C1 --> D2
C2 --> D3
C3 --> D4
C2 --> E1
C2 --> E2
C2 --> E3
2.2 核心子系统
子系统
核心职责
关键技术
账户系统
管理用户账户、商家账户、平台账户 - 余额查询、冻结/解冻 - 充值、提现 - 账户流水
- Redis + MySQL 双写 - 账户流水表 - 定时对账
支付网关
统一支付入口,屏蔽第三方差异 - 渠道抽象(适配器模式) - 路由策略(余额优先、组合支付) - 重试补偿
- 策略模式 - 适配器模式 - 异步回调
清结算引擎
分账计算和结算管理 - 分账规则(平台佣金、商家收益) - 结算周期(T+1、T+7) - 提现管理
- 定时任务 - 分账算法 - 限额控制
风控系统
保障资金安全 - 支付密码验证 - 异常交易监控 - 限额控制
- 规则引擎 - 实时监控 - 黑名单
2.3 设计原则 在架构设计中,遵循以下原则:
1. 领域边界清晰
账户、支付、清结算、对账等各司其职,通过明确的接口交互。
2. 事件驱动解耦
系统间通过 Kafka 事件解耦,支付服务发布”支付成功”事件,订单服务订阅并更新状态。
3. 可扩展性
支持新支付渠道接入(如数字货币)
支持新支付方式(如分期付款)
支持新业务场景(如预售、拼团)
4. 可观测性
结构化日志(JSON 格式)
全链路追踪(TraceID)
实时监控告警(Prometheus + Grafana)
面试加分项 :
能够在白板上快速画出以上架构图,并说明各层职责,会给面试官留下深刻印象。
三、核心业务流程 3.1 支付流程 3.1.1 时序图 sequenceDiagram
participant 用户
participant 订单服务
participant 支付服务
participant 支付网关
participant 第三方支付
用户->>订单服务: 1. 下单
订单服务->>支付服务: 2. 创建支付单
支付服务->>支付服务: 3. 幂等性检查
支付服务->>支付网关: 4. 渠道路由
支付网关->>第三方支付: 5. 调用支付接口
第三方支付-->>支付网关: 6. 同步返回
支付网关-->>用户: 7. 返回支付页面
用户->>第三方支付: 8. 完成支付
第三方支付->>支付网关: 9. 异步回调
支付网关->>支付服务: 10. 更新支付状态
支付服务->>订单服务: 11. 通知订单状态变更
支付服务->>用户: 12. 推送支付成功通知
3.1.2 关键步骤 Step 1: 创建支付单(幂等性保证)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 func CreatePaymentOrder (req *CreatePaymentRequest) (*PaymentOrder, error ) { idempotencyKey := fmt.Sprintf("%d_%d" , req.OrderID, req.UserID) lock := redis.Lock(idempotencyKey, 10 *time.Second) if !lock.TryLock() { return nil , errors.New("concurrent request, please retry" ) } defer lock.Unlock() existing := queryPaymentByIdempotencyKey(idempotencyKey) if existing != nil { return existing, nil } payment := &PaymentOrder{ PaymentID: snowflake.Generate(), OrderID: req.OrderID, UserID: req.UserID, PaymentAmount: req.Amount, PaymentStatus: "PENDING" , IdempotencyKey: idempotencyKey, } if err := db.Insert(payment); err != nil { if isDuplicateKeyError(err) { return queryPaymentByIdempotencyKey(idempotencyKey), nil } return nil , err } return payment, nil }
Step 2: 渠道路由
支付网关根据策略选择支付渠道:
余额优先策略 :用户余额足够则优先使用余额
组合支付 :余额不足时,余额 + 第三方支付
渠道降级 :主渠道不可用时切换备用渠道
Step 3: 异步回调处理(幂等性保证)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 func HandlePaymentCallback (callbackData *CallbackData) error { if !verifySignature(callbackData) { return errors.New("invalid signature" ) } existing := queryPaymentByChannelTradeNo(callbackData.ChannelTradeNo) if existing != nil && existing.PaymentStatus == "SUCCESS" { return nil } affected := db.Exec(` UPDATE payment_order SET payment_status = 'SUCCESS', channel_trade_no = ?, callback_time = ?, version = version + 1 WHERE payment_id = ? AND version = ? ` , callbackData.ChannelTradeNo, time.Now(), callbackData.PaymentID, callbackData.Version) if affected == 0 { return errors.New("concurrent update conflict" ) } publishPaymentSuccessEvent(callbackData.PaymentID, callbackData.OrderID) return nil }
Step 4: 重试机制
第三方回调可能失败,需要重试机制:
主动重试 :最多 3 次,指数退避(1s, 2s, 4s)
定时补偿 :每分钟扫描超时支付单,主动查询第三方状态
人工介入 :超过重试次数,进入人工复核
3.2 退款流程 3.2.1 时序图 sequenceDiagram
participant 用户
participant 订单服务
participant 支付服务
participant 第三方支付
participant 账户系统
用户->>订单服务: 1. 申请退款
订单服务->>支付服务: 2. 创建退款单
支付服务->>支付服务: 3. 校验可退金额
支付服务->>第三方支付: 4. 调用退款接口
第三方支付-->>支付服务: 5. 退款成功
支付服务->>账户系统: 6. 账户入账
支付服务->>订单服务: 7. 通知退款成功
支付服务->>用户: 8. 推送退款通知
3.2.2 可退金额计算 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 func CalculateRefundableAmount (paymentID int64 ) (*RefundableAmount, error ) { payment := queryPaymentOrder(paymentID) if payment.PaymentStatus != "SUCCESS" { return nil , errors.New("payment not success" ) } refundedAmount := sumRefundedAmount(paymentID) refundable := payment.PaymentAmount.Sub(refundedAmount) if refundable.LessThanOrEqual(decimal.Zero) { return nil , errors.New("no refundable amount" ) } result := &RefundableAmount{ TotalRefundable: refundable, } if payment.PromotionAmount.GreaterThan(decimal.Zero) { ratio := refundable.Div(payment.PaymentAmount) result.PromotionRefund = payment.PromotionAmount.Mul(ratio) result.ActualRefund = refundable.Sub(result.PromotionRefund) } else { result.ActualRefund = refundable } return result, nil }
3.2.3 部分退款 支持多次部分退款:
累计限制 :所有退款金额之和 ≤ 实付金额
退款记录 :每次退款都生成独立的退款单
状态联动 :全额退款后,支付单状态变为 REFUNDED
3.3 异步回调处理 graph LR
A[第三方回调] --> B{签名校验}
B -->|失败| C[拒绝并记录]
B -->|成功| D{幂等检查}
D -->|已处理| E[直接返回 SUCCESS]
D -->|未处理| F[更新支付状态]
F --> G[发布事件到 Kafka]
G --> H[返回 SUCCESS]
回调要点 :
签名校验 :防止伪造回调
幂等检查 :第三方交易号去重
乐观锁 :version 字段防止并发
事件驱动 :发布到 Kafka,解耦订单服务
四、状态机设计 状态机是支付系统的核心设计之一,清晰的状态定义和转换规则能够保证系统的稳定性。
4.1 支付单状态机 4.1.1 状态定义 stateDiagram-v2
[*] --> PENDING: 创建支付单
PENDING --> PAYING: 用户发起支付
PAYING --> SUCCESS: 第三方回调成功
PAYING --> FAILED: 第三方回调失败
PAYING --> CANCELED: 用户取消支付
SUCCESS --> REFUNDING: 用户申请退款
REFUNDING --> REFUNDED: 退款成功
FAILED --> [*]
CANCELED --> [*]
REFUNDED --> [*]
4.1.2 状态转换表
当前状态
允许的下一状态
触发条件
备注
PENDING
PAYING
用户发起支付
-
PAYING
SUCCESS
第三方回调成功
-
PAYING
FAILED
第三方回调失败
可重新发起支付
PAYING
CANCELED
用户取消支付
超时自动取消
SUCCESS
REFUNDING
用户申请退款
-
REFUNDING
REFUNDED
退款成功
支持部分退款
非法状态转换示例 :
PENDING → SUCCESS(跳过 PAYING 状态)
FAILED → REFUNDING(失败的支付单不能退款)
REFUNDED → SUCCESS(已退款不能恢复)
4.2 退款单状态机 4.2.1 状态定义 stateDiagram-v2
[*] --> PENDING: 创建退款单
PENDING --> PROCESSING: 调用第三方退款
PROCESSING --> SUCCESS: 退款成功
PROCESSING --> FAILED: 退款失败
SUCCESS --> [*]
FAILED --> [*]
4.2.2 与支付单状态联动
前置条件 :支付单必须是 SUCCESS 状态才能发起退款
状态同步 :退款成功后,支付单状态变为 REFUNDED
部分退款 :第一次退款成功后,支付单状态变为 PARTIAL_REFUNDED
4.3 状态机实现 4.3.1 状态转换校验 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 var stateTransitionRules = map [string ][]string { "PENDING" : {"PAYING" }, "PAYING" : {"SUCCESS" , "FAILED" , "CANCELED" }, "SUCCESS" : {"REFUNDING" }, "REFUNDING" : {"REFUNDED" }, } func isValidTransition (currentState, targetState string ) bool { allowedStates, exists := stateTransitionRules[currentState] if !exists { return false } for _, state := range allowedStates { if state == targetState { return true } } return false }
4.3.2 状态转换(乐观锁) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 func TransitState (paymentID int64 , targetState string , version int ) error { current := queryPaymentOrder(paymentID) if !isValidTransition(current.PaymentStatus, targetState) { return errors.New(fmt.Sprintf( "invalid state transition: %s -> %s" , current.PaymentStatus, targetState)) } affected := db.Exec(` UPDATE payment_order SET payment_status = ?, updated_at = ?, version = version + 1 WHERE payment_id = ? AND version = ? ` , targetState, time.Now(), paymentID, version) if affected == 0 { return errors.New("concurrent update conflict, please retry" ) } publishStateChangeEvent(paymentID, current.PaymentStatus, targetState) return nil }
4.3.3 状态变更事件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 type PaymentStateChangeEvent struct { PaymentID int64 `json:"payment_id"` OrderID int64 `json:"order_id"` OldState string `json:"old_state"` NewState string `json:"new_state"` ChangeTime time.Time `json:"change_time"` } func publishStateChangeEvent (paymentID int64 , oldState, newState string ) { event := &PaymentStateChangeEvent{ PaymentID: paymentID, OrderID: getOrderIDByPaymentID(paymentID), OldState: oldState, NewState: newState, ChangeTime: time.Now(), } kafka.Publish("payment_state_change" , event) }
面试要点 :
为什么需要状态机 :确保状态转换的合法性,防止业务逻辑错误
如何保证并发安全 :乐观锁(version 字段)
如何与其他系统协作 :通过 Kafka 事件驱动
五、高频考点深入 5.1 分布式事务 5.1.1 问题场景 面试问题 :支付成功后,如何保证订单状态同步更新?
这是一个典型的分布式事务问题:
支付服务:更新支付单状态为 SUCCESS
订单服务:更新订单状态为 PAID
两个操作在不同的服务和数据库中,无法使用传统的 ACID 事务保证一致性。
5.1.2 Saga 模式 Saga 模式是微服务架构下常用的分布式事务解决方案,通过一系列本地事务和补偿机制实现最终一致性。
Saga 流程图 :
sequenceDiagram
participant 订单服务
participant 支付服务
participant 本地消息表
participant Kafka
订单服务->>订单服务: 1. 创建订单
订单服务->>支付服务: 2. 请求支付
支付服务->>支付服务: 3. 更新支付状态 SUCCESS
支付服务->>本地消息表: 4. 插入"订单已支付"事件
支付服务->>支付服务: 5. 提交本地事务
本地消息表->>Kafka: 6. 定时任务扫描并发送
Kafka->>订单服务: 7. 订单服务消费事件
订单服务->>订单服务: 8. 更新订单状态 PAID
本地消息表实现 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 type LocalMessage struct { ID int64 `db:"id"` OrderID int64 `db:"order_id"` PaymentID int64 `db:"payment_id"` Event string `db:"event"` Status string `db:"status"` RetryCount int `db:"retry_count"` CreatedAt time.Time `db:"created_at"` } func OnPaymentSuccess (paymentID int64 , orderID int64 ) error { tx, _ := db.Begin() tx.Exec(` UPDATE payment_order SET payment_status = 'SUCCESS' WHERE payment_id = ? ` , paymentID) tx.Exec(` INSERT INTO local_message (order_id, payment_id, event, status, retry_count) VALUES (?, ?, 'ORDER_PAID', 'PENDING', 0) ` , orderID, paymentID) return tx.Commit() } func ScanAndSendMessages () { messages := db.Query(` SELECT * FROM local_message WHERE status = 'PENDING' AND retry_count < 3 ORDER BY created_at ASC LIMIT 100 ` ) for _, msg := range messages { if err := kafka.Publish("order_paid" , msg); err == nil { db.Exec(` UPDATE local_message SET status = 'SENT' WHERE id = ? ` , msg.ID) } else { db.Exec(` UPDATE local_message SET retry_count = retry_count + 1 WHERE id = ? ` , msg.ID) } } }
补偿机制 :
如果支付失败,订单服务需要取消订单:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func OnPaymentFailed (paymentID int64 , orderID int64 ) error { db.Exec(`UPDATE payment_order SET payment_status = 'FAILED' WHERE payment_id = ?` , paymentID) kafka.Publish("payment_failed" , &PaymentFailedEvent{ PaymentID: paymentID, OrderID: orderID, }) return nil }
5.1.3 TCC 模式 TCC(Try-Confirm-Cancel)模式适用于对一致性要求较高的场景,如退款流程。
TCC 三阶段 :
sequenceDiagram
participant 订单服务
participant 支付服务
participant 账户系统
订单服务->>账户系统: Try: 冻结账户余额
订单服务->>支付服务: Try: 创建退款单
alt 全部成功
订单服务->>账户系统: Confirm: 解冻并扣减
订单服务->>支付服务: Confirm: 退款成功
else 任一失败
订单服务->>账户系统: Cancel: 直接解冻
订单服务->>支付服务: Cancel: 取消退款
end
TCC 实现示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 func TryFreezeBalance (userID int64 , amount decimal.Decimal) (string , error ) { freezeID := uuid.New().String() affected := db.Exec(` UPDATE account SET balance = balance - ?, frozen_balance = frozen_balance + ? WHERE user_id = ? AND balance >= ? ` , amount, amount, userID, amount) if affected == 0 { return "" , errors.New("insufficient balance" ) } db.Exec(` INSERT INTO account_freeze (freeze_id, user_id, amount, status) VALUES (?, ?, ?, 'FROZEN') ` , freezeID, userID, amount) return freezeID, nil } func ConfirmFreezeBalance (freezeID string ) error { freeze := db.QueryRow(` SELECT user_id, amount FROM account_freeze WHERE freeze_id = ? AND status = 'FROZEN' ` , freezeID) db.Exec(` UPDATE account_freeze SET status = 'CONFIRMED' WHERE freeze_id = ? ` , freezeID) db.Exec(` UPDATE account SET frozen_balance = frozen_balance - ? WHERE user_id = ? ` , freeze.Amount, freeze.UserID) return nil } func CancelFreezeBalance (freezeID string ) error { freeze := db.QueryRow(` SELECT user_id, amount FROM account_freeze WHERE freeze_id = ? AND status = 'FROZEN' ` , freezeID) db.Exec(` UPDATE account SET balance = balance + ?, frozen_balance = frozen_balance - ? WHERE user_id = ? ` , freeze.Amount, freeze.Amount, freeze.UserID) db.Exec(` UPDATE account_freeze SET status = 'CANCELED' WHERE freeze_id = ? ` , freezeID) return nil }
5.1.4 Saga vs TCC
维度
Saga
TCC
一致性
最终一致性
强一致性
实现复杂度
低
高
性能
高(异步)
中(同步)
适用场景
支付流程
退款流程
回滚方式
补偿操作
Cancel 操作
面试建议 :
Saga 适合长事务、跨服务场景
TCC 适合短事务、强一致性场景
5.2 幂等性设计 5.2.1 为什么需要幂等
第三方回调重复 :网络抖动导致重复回调
用户重复点击 :前端未防抖,用户多次点击
系统重试 :超时重试导致重复请求
5.2.2 三个关键场景 场景 1: 支付单创建幂等
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 func CreatePaymentOrder (orderID int64 , userID int64 , amount decimal.Decimal) (*PaymentOrder, error ) { idempotencyKey := fmt.Sprintf("payment_%d_%d" , orderID, userID) lock := redis.Lock(idempotencyKey, 10 *time.Second) if !lock.TryLock() { return nil , errors.New("concurrent request" ) } defer lock.Unlock() existing := db.QueryRow(` SELECT * FROM payment_order WHERE order_id = ? AND user_id = ? ` , orderID, userID) if existing != nil { return existing, nil } payment := &PaymentOrder{ PaymentID: snowflake.Generate(), OrderID: orderID, UserID: userID, Amount: amount, } if err := db.Insert(payment); err != nil { if isDuplicateKeyError(err) { return db.QueryRow(`SELECT * FROM payment_order WHERE order_id = ? AND user_id = ?` , orderID, userID), nil } return nil , err } return payment, nil }
场景 2: 支付回调幂等
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 func HandlePaymentCallback (channelTradeNo string , status string ) error { existing := db.QueryRow(` SELECT * FROM payment_order WHERE channel_trade_no = ? ` , channelTradeNo) if existing != nil && existing.PaymentStatus == "SUCCESS" { return nil } db.Exec(` UPDATE payment_order SET payment_status = ?, channel_trade_no = ? WHERE payment_id = ? ` , status, channelTradeNo, existing.PaymentID) return nil }
场景 3: 退款幂等
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 func CreateRefundOrder (paymentID int64 , amount decimal.Decimal) (*RefundOrder, error ) { refundID := snowflake.Generate() existing := db.QueryRow(` SELECT * FROM refund_order WHERE payment_id = ? AND amount = ? ` , paymentID, amount) if existing != nil { return existing, nil } refund := &RefundOrder{ RefundID: refundID, PaymentID: paymentID, Amount: amount, Status: "PENDING" , } db.Insert(refund) return refund, nil }
5.2.3 实现手段总结
手段
使用场景
优点
缺点
Redis 分布式锁
高并发场景
性能好,防并发
需要考虑锁超时
数据库唯一索引
防重复插入
可靠,数据库保证
性能略低
乐观锁
防并发更新
无锁开销
需要重试
5.3 一致性保证 5.3.1 账户余额一致性 问题 :Redis 缓存和 MySQL 数据不一致
方案 :Redis + MySQL 双写 + Lua 脚本 + 定时对账
1 2 3 4 5 6 7 8 9 10 11 12 local balance_key = KEYS[1 ]local amount = tonumber (ARGV[1 ])local balance = tonumber (redis.call('GET' , balance_key) or '0' )if balance >= amount then redis.call('DECRBY' , balance_key, amount) return 1 else return 0 end
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 func DeductBalance (userID int64 , amount decimal.Decimal) error { result := redis.Eval(luaScript, []string {fmt.Sprintf("balance:%d" , userID)}, amount.String()) if result == 0 { return errors.New("insufficient balance" ) } affected := db.Exec(` UPDATE account SET balance = balance - ? WHERE user_id = ? AND balance >= ? ` , amount, userID, amount) if affected == 0 { redis.IncrBy(fmt.Sprintf("balance:%d" , userID), amount.IntPart()) return errors.New("insufficient balance" ) } db.Exec(` INSERT INTO account_transaction (user_id, amount, type, created_at) VALUES (?, ?, 'DEDUCT', NOW()) ` , userID, amount) return nil } func ReconcileAccountBalance () { users := db.Query("SELECT user_id FROM account" ) for _, user := range users { mysqlBalance := db.QueryRow(`SELECT balance FROM account WHERE user_id = ?` , user.ID) redisBalance := redis.Get(fmt.Sprintf("balance:%d" , user.ID)) if mysqlBalance != redisBalance { log.Error("balance mismatch" , "user_id" , user.ID, "mysql" , mysqlBalance, "redis" , redisBalance) redis.Set(fmt.Sprintf("balance:%d" , user.ID), mysqlBalance) } } }
5.3.2 支付流水一致性 问题 :支付单金额与流水表汇总金额不一致
方案 :
每笔支付/退款都记录流水
流水表不可更新,只能插入
定时任务校验:支付单金额 = 流水表汇总金额
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 func ReconcilePaymentTransaction () { payments := db.Query("SELECT payment_id, payment_amount FROM payment_order WHERE payment_status = 'SUCCESS'" ) for _, payment := range payments { transactionAmount := db.QueryRow(` SELECT SUM(amount) FROM payment_transaction WHERE payment_id = ? ` , payment.PaymentID) if payment.PaymentAmount != transactionAmount { log.Error("amount mismatch" , "payment_id" , payment.PaymentID, "payment" , payment.PaymentAmount, "transaction" , transactionAmount) notifyAdmin(payment.PaymentID) } } }
面试要点 :
一致性方案 : Redis + MySQL 双写 + 定时对账
原子性保证 : Lua 脚本
数据源优先级 : MySQL 为准,Redis 为辅
六、扩展模块 本章简要介绍支付系统的扩展模块,这些模块在面试中通常不是重点,但了解基本概念有助于建立完整的知识体系。
6.1 清结算系统 6.1.1 核心概念 清结算系统负责将支付金额按规则分配给平台、商家、营销补贴等各方。
分账规则示例 :
假设一笔订单实付 100 元,分账如下:
角色
比例
金额
平台佣金
5%
5 元
商家收益
95%
95 元
营销补贴
-
由平台承担
6.1.2 结算周期
T+1 :次日结算(快速到账,适合小商家)
T+7 :7 日后结算(标准周期)
账期结算 :按月结算(企业客户)
6.1.3 提现管理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 func WithdrawRequest (merchantID int64 , amount decimal.Decimal) error { if amount.GreaterThan(decimal.NewFromInt(50000 )) { return errors.New("single withdraw limit exceeded: 50,000" ) } todayWithdrawn := db.QueryRow(` SELECT SUM(amount) FROM withdraw_order WHERE merchant_id = ? AND DATE(created_at) = CURDATE() ` , merchantID) if todayWithdrawn.Add(amount).GreaterThan(decimal.NewFromInt(200000 )) { return errors.New("daily withdraw limit exceeded: 200,000" ) } if isRiskMerchant(merchantID) { return errors.New("risk merchant, withdraw suspended" ) } withdraw := &WithdrawOrder{ WithdrawID: snowflake.Generate(), MerchantID: merchantID, Amount: amount, Status: "PENDING" , } db.Insert(withdraw) return nil }
6.1.4 清结算流程 graph LR
A[T 日交易] --> B[T+1 日凌晨定时任务]
B --> C[计算分账金额]
C --> D[生成结算单]
D --> E[商家确认]
E --> F[提现申请]
F --> G[打款到银行账户]
G --> H[更新账户余额]
6.2 对账系统 6.2.1 为什么需要对账 系统与第三方的交易数据可能存在以下问题:
长款 :第三方有,本地无(用户已支付,但系统未记录)
短款 :本地有,第三方无(系统记录已支付,但第三方未收到)
金额不符 :订单号一致,但金额不一致
6.2.2 对账维度
对账维度
对账内容
数据来源
交易对账
订单号、金额、状态
第三方对账文件 vs 本地支付流水
资金对账
入账金额、手续费
第三方结算单 vs 本地账户流水
6.2.3 对账流程 graph LR
A[每日凌晨 2:00] --> B[拉取第三方对账文件]
B --> C[解析对账文件]
C --> D[与本地流水对比]
D --> E[生成差错报告]
E --> F{有差错?}
F -->|是| G[人工复核]
F -->|否| H[记录对账结果]
G --> I[差错处理]
6.2.4 差错处理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 type ReconciliationError struct { Type string OrderID int64 LocalAmount decimal.Decimal RemoteAmount decimal.Decimal Description string } func ReconcileTransactions (date time.Time) ([]*ReconciliationError, error ) { var errors []*ReconciliationError remoteRecords := fetchRemoteReconciliationFile(date) localRecords := db.Query(` SELECT order_id, amount FROM payment_transaction WHERE DATE(created_at) = ? ` , date) for _, local := range localRecords { if !existsInRemote(local.OrderID, remoteRecords) { errors = append (errors, &ReconciliationError{ Type: "SHORT_PAYMENT" , OrderID: local.OrderID, LocalAmount: local.Amount, Description: "本地有记录,第三方无" , }) } } for _, remote := range remoteRecords { if !existsInLocal(remote.OrderID, localRecords) { errors = append (errors, &ReconciliationError{ Type: "LONG_PAYMENT" , OrderID: remote.OrderID, RemoteAmount: remote.Amount, Description: "第三方有记录,本地无" , }) } } for _, local := range localRecords { remote := findRemoteRecord(local.OrderID, remoteRecords) if remote != nil && !local.Amount.Equal(remote.Amount) { errors = append (errors, &ReconciliationError{ Type: "AMOUNT_MISMATCH" , OrderID: local.OrderID, LocalAmount: local.Amount, RemoteAmount: remote.Amount, Description: "金额不一致" , }) } } return errors, nil }
6.3 风控系统 6.3.1 三个阶段
阶段
风控手段
示例
事前风控
支付前验证
支付密码、指纹、人脸识别
事中风控
交易实时监控
短时间大额、异地登录、限额控制
事后风控
对账差错分析
资金流向追踪、异常模式识别
6.3.2 常见风控规则 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 var riskRules = []RiskRule{ { Name: "单笔大额" , Condition: "amount > 10000" , Action: "二次验证" , Description: "单笔支付超过 10,000 元需要二次验证" , }, { Name: "短时间高频" , Condition: "count_in_1h > 5" , Action: "触发风控审核" , Description: "1 小时内支付超过 5 次" , }, { Name: "异地登录" , Condition: "ip_city_change" , Action: "短信验证码" , Description: "IP 地址城市变更" , }, } func RiskCheck (userID int64 , amount decimal.Decimal) error { if amount.GreaterThan(decimal.NewFromInt(10000 )) { return errors.New("need second verification for large amount" ) } count := redis.Incr(fmt.Sprintf("payment_count:%d" , userID)) redis.Expire(fmt.Sprintf("payment_count:%d" , userID), 1 *time.Hour) if count > 5 { return errors.New("too many payments in 1 hour" ) } lastCity := redis.Get(fmt.Sprintf("last_city:%d" , userID)) currentCity := getIPCity() if lastCity != "" && lastCity != currentCity { return errors.New("city changed, need SMS verification" ) } redis.Set(fmt.Sprintf("last_city:%d" , userID), currentCity) return nil }
6.3.3 黑名单机制 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func IsBlacklisted (userID int64 ) bool { return redis.SIsMember("user_blacklist" , userID) } func AddToBlacklist (userID int64 , reason string ) { redis.SAdd("user_blacklist" , userID) db.Exec(` INSERT INTO blacklist_record (user_id, reason, created_at) VALUES (?, ?, NOW()) ` , userID, reason)}
面试要点 :
清结算:分账规则、结算周期、提现限额
对账:长款、短款、差错处理
风控:事前、事中、事后三阶段
七、面试问答锦囊 本章总结支付系统面试中最常被问到的问题,并给出简洁的回答要点,便于考前快速复习。
7.1 架构设计类 Q1: 如何设计一个支付系统?⭐⭐⭐ 回答框架 (3 分钟讲清楚):
分层架构 :
接入层(C 端/B 端/Open API)
应用服务层(订单、支付、对账服务)
核心业务层(账户、支付网关、清结算、风控)
基础设施层(MySQL、Redis、Kafka、XXL-Job)
第三方服务层(微信支付、支付宝、银行网关)
核心子系统 :
账户系统:余额管理、冻结/解冻
支付网关:渠道抽象、路由策略
清结算引擎:分账计算、结算管理
风控系统:支付验证、异常监控
关键技术 :
分布式事务:Saga/TCC
幂等性设计:分布式锁 + 唯一索引
状态机:支付单/退款单状态流转
白板画图要点 :
1 2 3 4 5 6 7 8 9 10 11 ┌─────────────────────────────────┐ │ 接入层(C/B 端) │ ├─────────────────────────────────┤ │ 应用层(订单/支付/对账) │ ├─────────────────────────────────┤ │ 核心层(账户/网关/清结算) │ ├─────────────────────────────────┤ │ 基础设施(MySQL/Redis/Kafka) │ ├─────────────────────────────────┤ │ 第三方(微信/支付宝) │ └─────────────────────────────────┘
详见 :第 2 章
Q2: 支付系统如何保证高可用?⭐⭐ 回答要点 (5 个维度):
多机房部署 :异地容灾,主备切换
Redis 主从 + 哨兵 :缓存高可用,故障自动切换
Kafka 集群 :消息高可用,分区副本机制
限流降级 :大促期间保护核心服务
熔断机制 :第三方支付故障时切换备用渠道
详见 :第 1.3 章
7.2 技术方案类 Q3: 支付成功后如何保证订单状态同步更新?⭐⭐⭐ 回答要点 :
Saga 模式 + 本地消息表 + 最终一致性
实现步骤 :
支付服务更新支付状态 + 插入本地消息表(同一事务)
定时任务扫描本地消息表,发送到 Kafka
订单服务消费 Kafka 消息,更新订单状态
补偿机制 :
详见 :第 5.1.2 章
Q4: 如何保证支付幂等性?⭐⭐⭐ 回答要点 (三个场景):
场景
幂等键
实现手段
支付单创建
order_id + user_id
Redis 分布式锁 + DB 唯一索引
支付回调
channel_trade_no
DB 唯一索引
退款
refund_id
DB 唯一索引
核心思想 :
防并发 :Redis 分布式锁
防重复 :数据库唯一索引
防并发更新 :乐观锁(version 字段)
详见 :第 5.2 章
Q5: 第三方支付回调失败怎么办?⭐⭐ 回答要点 (三层保障):
主动重试 :最多 3 次,指数退避(1s, 2s, 4s)
定时补偿 :每分钟扫描超时支付单,主动查询第三方状态
人工介入 :超过重试次数,进入人工复核队列
详见 :第 3.3 章
Q6: 如何处理部分退款?⭐⭐ 回答要点 :
可退金额计算 :实付金额 - 已退金额
多次退款支持 :累计退款金额 ≤ 实付金额
营销优惠处理 :按比例退款
示例 :
实付 100 元(原价 120,优惠 20)
退款 50 元 → 退实付 41.67,营销优惠退 8.33
详见 :第 3.2.2 章
7.3 场景题类 Q7: 大促期间支付峰值如何应对?⭐⭐ 回答要点 (4 个维度):
维度
方案
说明
缓存
Redis 缓存热点数据
账户余额、支付单状态
限流
令牌桶限流
单用户 QPS 限制
降级
关闭非核心功能
账单查询、历史记录
异步化
Kafka 异步处理
支付回调、状态同步
详见 :第 1.3 章
Q8: 如何防止恶意刷单?⭐ 回答要点 (风控规则):
规则
阈值
动作
短时间大额
单笔 > 10,000 元
二次验证
短时间高频
1 小时内 > 5 笔
触发风控审核
异地登录
IP 城市变更
短信验证码
限额控制
单日 > 200,000 元
暂停支付
详见 :第 6.3.2 章
7.4 面试技巧 1. 架构设计题
快速画出分层架构图(30 秒)
说明核心子系统职责(1 分钟)
讲解关键技术方案(2 分钟)
2. 技术方案题
先说核心思想(10 秒)
再讲实现步骤(1 分钟)
最后补充边界情况(30 秒)
3. 场景题
先识别核心问题(10 秒)
提出多个解决方案(1 分钟)
对比优劣并推荐(30 秒)
4. 常见追问
“如果第三方支付挂了怎么办?” → 渠道降级、备用渠道
“如何保证资金安全?” → 分布式事务、幂等性、审计日志
“支付系统的瓶颈在哪?” → 数据库写入、第三方支付 QPS
总结 本文从系统设计面试的角度,深入解析了电商支付系统的核心知识点:
整体架构 :分层架构、核心子系统、设计原则
核心流程 :支付流程、退款流程、异步回调
状态机设计 :支付单/退款单状态流转、状态转换规则
高频考点 :
分布式事务(Saga/TCC)
幂等性设计(三个场景)
一致性保证(账户余额、支付流水)
扩展模块 :清结算、对账、风控
面试锦囊 :8 个高频面试题及回答要点
面试建议 :
熟练掌握分层架构图,能够在白板上快速画出
深入理解分布式事务(Saga/TCC)的实现原理
掌握幂等性设计的三个关键场景
了解清结算、对账、风控的基本概念
参考资料 :
全文完