如何设计支付系统
一、问题描述
1.1 业务背景
支付系统是电商、金融平台的核心,直接涉及资金安全。
典型应用:
- 电商支付:淘宝、京东支付
- 第三方支付:支付宝、微信支付
- 金融支付:银行转账、理财
核心要求:
- 资金安全:一分钱都不能错
- 数据一致性:强一致性
- 高可用:99.99%以上
1.2 核心功能
基础功能:
- 发起支付:创建支付订单
- 支付确认:完成支付
- 支付回调:接收支付结果
- 支付查询:查询支付状态
进阶功能:
- 退款:支付退回
- 对账:定时对账
- 风控:异常检测
1.3 技术挑战
幂等性:
用户点击支付,网络超时重试
如何保证不重复扣款?一致性:
扣款成功,但订单状态未更新
如何保证状态一致?资金安全:
如何保证账户余额不会错?
如何防止黑客攻击?1.4 面试考察点
- 支付流程:完整的状态机设计
- 幂等性:Token机制
- 账户模型:借贷记账法
- 对账系统:定时对账和差错处理
二、支付流程
2.1 完整流程图
mermaid
sequenceDiagram
participant U as 用户
participant M as 商户系统
participant P as 支付系统
participant C as 支付渠道
U->>M: 1. 提交订单
M->>P: 2. 创建支付订单
P-->>M: 3. 返回支付订单号
M->>P: 4. 发起支付
P->>P: 5. 检查幂等Token
P->>P: 6. 扣减账户余额
P->>C: 7. 调用第三方支付
C-->>P: 8. 返回支付结果
alt 支付成功
P->>M: 9. 异步回调通知
M->>M: 10. 更新订单状态
M-->>P: 11. 确认收到回调
P-->>U: 12. 支付成功
else 支付失败
P->>P: 13. 回滚账户余额
P-->>U: 14. 支付失败
end2.2 支付状态机
mermaid
stateDiagram-v2
[*] --> 待支付
待支付 --> 支付中: 发起支付
支付中 --> 支付成功: 支付完成
支付中 --> 支付失败: 支付失败
支付成功 --> 已退款: 退款
支付失败 --> 待支付: 重新支付三、账户模型
3.1 借贷记账法
原理:每笔交易都有借方和贷方,借贷相等。
账户表:
sql
CREATE TABLE account (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT UNIQUE NOT NULL,
balance BIGINT DEFAULT 0 COMMENT '余额(分)',
frozen_balance BIGINT DEFAULT 0 COMMENT '冻结金额(分)',
version INT DEFAULT 0 COMMENT '版本号(乐观锁)',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
KEY idx_user (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
COMMENT='账户表';流水表(关键):
sql
CREATE TABLE account_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
trade_no VARCHAR(64) UNIQUE NOT NULL COMMENT '交易流水号',
user_id BIGINT NOT NULL,
amount BIGINT NOT NULL COMMENT '金额(分)',
balance_before BIGINT NOT NULL COMMENT '交易前余额',
balance_after BIGINT NOT NULL COMMENT '交易后余额',
trade_type TINYINT NOT NULL COMMENT '交易类型 1充值 2消费 3退款',
biz_type VARCHAR(32) COMMENT '业务类型',
biz_id VARCHAR(64) COMMENT '业务ID',
remark VARCHAR(500),
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
KEY idx_user (user_id, create_time),
KEY idx_trade_no (trade_no)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
COMMENT='账户流水表';3.2 借贷记账示例
场景:用户A向用户B转账100元
go
// 借方(A账户减少)
{
UserID: A,
Amount: -100,
BalanceBefore: 1000,
BalanceAfter: 900,
TradeType: 2, // 消费
}
// 贷方(B账户增加)
{
UserID: B,
Amount: 100,
BalanceBefore: 500,
BalanceAfter: 600,
TradeType: 1, // 充值
}
// 校验:借贷相等
Sum(借方) + Sum(贷方) = -100 + 100 = 0 ✅四、幂等性设计
4.1 Token机制
流程:
- 发起支付前,获取幂等Token
- 提交支付时,携带Token
- 支付系统校验Token,成功后立即删除
- 重复请求时,Token已不存在,返回"处理中"
实现:
go
// 1. 获取Token
func (s *PaymentService) GetIdempotentToken(userID int64) (string, error) {
token := generateToken() // 生成UUID
// 存储到Redis,5分钟有效
key := fmt.Sprintf("payment:token:%s", token)
s.redis.Set(ctx, key, userID, 5*time.Minute)
return token, nil
}
// 2. 校验并消费Token
func (s *PaymentService) ConsumeToken(token string, userID int64) error {
key := fmt.Sprintf("payment:token:%s", token)
// 使用Lua脚本原子性检查+删除
script := `
local val = redis.call('GET', KEYS[1])
if not val then
return 0 -- Token不存在或已使用
end
if val ~= ARGV[1] then
return -1 -- 用户不匹配
end
redis.call('DEL', KEYS[1])
return 1 -- 成功
`
result, err := s.redis.Eval(ctx, script, []string{key}, userID).Int()
if err != nil {
return err
}
if result == 0 {
return errors.New("Token已使用,请勿重复提交")
}
if result == -1 {
return errors.New("Token不匹配")
}
return nil
}4.2 唯一键约束
sql
-- 支付订单表:trade_no唯一
CREATE TABLE payment_order (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
trade_no VARCHAR(64) UNIQUE NOT NULL,
...
);
-- 插入时自动去重
INSERT INTO payment_order (trade_no, ...) VALUES (?, ...);
-- 重复插入会报错:Duplicate entry五、核心实现
5.1 Go实现
点击查看完整实现
go
package payment
import (
"context"
"fmt"
"time"
"gorm.io/gorm"
)
type PaymentService struct {
db *gorm.DB
redis *redis.Client
}
type PaymentOrder struct {
ID int64
TradeNo string // 交易流水号
OutTradeNo string // 商户订单号
UserID int64
Amount int64 // 金额(分)
Channel string // 支付渠道
Status int8 // 1待支付 2支付中 3成功 4失败
CreateTime time.Time
}
type Account struct {
ID int64
UserID int64
Balance int64
FrozenBalance int64
Version int
}
type AccountLog struct {
ID int64
TradeNo string
UserID int64
Amount int64
BalanceBefore int64
BalanceAfter int64
TradeType int8
CreateTime time.Time
}
// CreatePaymentOrder 创建支付订单
func (s *PaymentService) CreatePaymentOrder(ctx context.Context, req *CreateOrderReq) (*PaymentOrder, error) {
order := &PaymentOrder{
TradeNo: generateTradeNo(),
OutTradeNo: req.OutTradeNo,
UserID: req.UserID,
Amount: req.Amount,
Channel: req.Channel,
Status: 1, // 待支付
CreateTime: time.Now(),
}
err := s.db.Create(order).Error
if err != nil {
return nil, err
}
return order, nil
}
// Pay 发起支付
func (s *PaymentService) Pay(ctx context.Context, req *PayReq) error {
// 1. 校验幂等Token
err := s.ConsumeToken(req.IdempotentToken, req.UserID)
if err != nil {
return err
}
// 2. 查询支付订单
var order PaymentOrder
err = s.db.Where("trade_no = ?", req.TradeNo).First(&order).Error
if err != nil {
return err
}
// 3. 检查订单状态
if order.Status != 1 {
return fmt.Errorf("订单状态异常")
}
// 4. 更新订单状态为支付中
s.db.Model(&order).Update("status", 2)
// 5. 扣减账户余额
err = s.DeductBalance(ctx, req.UserID, order.Amount, order.TradeNo)
if err != nil {
// 回滚订单状态
s.db.Model(&order).Update("status", 4)
return err
}
// 6. 调用第三方支付(省略)
// ...
// 7. 更新订单状态为成功
s.db.Model(&order).Update("status", 3)
return nil
}
// DeductBalance 扣减账户余额
func (s *PaymentService) DeductBalance(ctx context.Context, userID, amount int64, tradeNo string) error {
return s.db.Transaction(func(tx *gorm.DB) error {
// 1. 锁定账户行
var account Account
err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("user_id = ?", userID).
First(&account).Error
if err != nil {
return err
}
// 2. 检查余额
if account.Balance < amount {
return fmt.Errorf("余额不足")
}
// 3. 扣减余额(乐观锁)
affected := tx.Model(&account).
Where("version = ?", account.Version).
Updates(map[string]interface{}{
"balance": gorm.Expr("balance - ?", amount),
"version": gorm.Expr("version + 1"),
}).RowsAffected
if affected == 0 {
return fmt.Errorf("账户版本冲突,请重试")
}
// 4. 记录流水
log := &AccountLog{
TradeNo: tradeNo,
UserID: userID,
Amount: -amount, // 负数表示支出
BalanceBefore: account.Balance,
BalanceAfter: account.Balance - amount,
TradeType: 2, // 消费
CreateTime: time.Now(),
}
err = tx.Create(log).Error
if err != nil {
return err
}
return nil
})
}
// PaymentCallback 支付回调
func (s *PaymentService) PaymentCallback(ctx context.Context, req *CallbackReq) error {
// 1. 验证签名(省略)
// ...
// 2. 查询订单
var order PaymentOrder
err := s.db.Where("trade_no = ?", req.TradeNo).First(&order).Error
if err != nil {
return err
}
// 3. 幂等性检查
if order.Status == 3 {
return nil // 已处理,直接返回成功
}
// 4. 更新订单状态
if req.PayResult == "SUCCESS" {
s.db.Model(&order).Update("status", 3)
// 5. 通知商户(异步)
s.notifyMerchant(order.OutTradeNo, "SUCCESS")
} else {
s.db.Model(&order).Update("status", 4)
// 回滚账户余额
s.RefundBalance(ctx, order.UserID, order.Amount, order.TradeNo)
}
return nil
}
// Refund 退款
func (s *PaymentService) Refund(ctx context.Context, tradeNo string) error {
// 1. 查询原支付订单
var order PaymentOrder
err := s.db.Where("trade_no = ?", tradeNo).First(&order).Error
if err != nil {
return err
}
// 2. 检查订单状态
if order.Status != 3 {
return fmt.Errorf("订单未支付成功,无法退款")
}
// 3. 调用第三方退款接口(省略)
// ...
// 4. 退回账户余额
return s.RefundBalance(ctx, order.UserID, order.Amount, tradeNo+"-REFUND")
}
// RefundBalance 退款到账户
func (s *PaymentService) RefundBalance(ctx context.Context, userID, amount int64, tradeNo string) error {
return s.db.Transaction(func(tx *gorm.DB) error {
// 1. 增加余额
var account Account
tx.Where("user_id = ?", userID).First(&account)
tx.Model(&account).Updates(map[string]interface{}{
"balance": gorm.Expr("balance + ?", amount),
"version": gorm.Expr("version + 1"),
})
// 2. 记录流水
log := &AccountLog{
TradeNo: tradeNo,
UserID: userID,
Amount: amount, // 正数表示收入
BalanceBefore: account.Balance,
BalanceAfter: account.Balance + amount,
TradeType: 3, // 退款
CreateTime: time.Now(),
}
return tx.Create(log).Error
})
}
// Reconcile 对账
func (s *PaymentService) Reconcile(ctx context.Context, date string) error {
// 1. 从第三方获取对账单
thirdPartyOrders := s.fetchThirdPartyOrders(date)
// 2. 从数据库获取订单
var localOrders []PaymentOrder
s.db.Where("DATE(create_time) = ?", date).Find(&localOrders)
// 3. 对比差异
thirdMap := make(map[string]int64)
for _, o := range thirdPartyOrders {
thirdMap[o.TradeNo] = o.Amount
}
localMap := make(map[string]int64)
for _, o := range localOrders {
localMap[o.TradeNo] = o.Amount
}
// 4. 找出差异订单
var diffOrders []string
// 本地有,第三方没有
for tradeNo := range localMap {
if _, exists := thirdMap[tradeNo]; !exists {
diffOrders = append(diffOrders, tradeNo)
}
}
// 第三方有,本地没有
for tradeNo := range thirdMap {
if _, exists := localMap[tradeNo]; !exists {
diffOrders = append(diffOrders, tradeNo)
}
}
// 5. 告警差异订单
if len(diffOrders) > 0 {
s.alertDiffOrders(diffOrders)
}
return nil
}
func generateTradeNo() string {
return fmt.Sprintf("PAY%d", time.Now().UnixNano())
}5.2 Java实现
java
@Service
public class PaymentService {
@Autowired
private PaymentOrderMapper orderMapper;
@Autowired
private AccountMapper accountMapper;
@Autowired
private AccountLogMapper logMapper;
/**
* 发起支付
*/
@Transactional
public void pay(PayRequest request) {
// 1. 校验幂等Token
consumeToken(request.getIdempotentToken(), request.getUserId());
// 2. 查询支付订单
PaymentOrder order = orderMapper.findByTradeNo(request.getTradeNo());
if (order.getStatus() != 1) {
throw new BusinessException("订单状态异常");
}
// 3. 更新订单状态为支付中
orderMapper.updateStatus(order.getId(), 2);
try {
// 4. 扣减账户余额
deductBalance(request.getUserId(), order.getAmount(), order.getTradeNo());
// 5. 调用第三方支付
// ...
// 6. 更新订单状态为成功
orderMapper.updateStatus(order.getId(), 3);
} catch (Exception e) {
// 回滚订单状态
orderMapper.updateStatus(order.getId(), 4);
throw e;
}
}
/**
* 扣减账户余额
*/
private void deductBalance(Long userId, Long amount, String tradeNo) {
// 1. 锁定账户
Account account = accountMapper.lockByUserId(userId);
// 2. 检查余额
if (account.getBalance() < amount) {
throw new BusinessException("余额不足");
}
// 3. 扣减余额(乐观锁)
int affected = accountMapper.deductBalance(
userId,
amount,
account.getVersion()
);
if (affected == 0) {
throw new BusinessException("账户版本冲突,请重试");
}
// 4. 记录流水
AccountLog log = new AccountLog();
log.setTradeNo(tradeNo);
log.setUserId(userId);
log.setAmount(-amount);
log.setBalanceBefore(account.getBalance());
log.setBalanceAfter(account.getBalance() - amount);
log.setTradeType((byte) 2);
logMapper.insert(log);
}
}六、对账系统
6.1 对账流程
mermaid
graph LR
A[定时任务] --> B[获取第三方对账单]
A --> C[获取本地订单]
B --> D[数据对比]
C --> D
D --> E{是否一致?}
E -->|是| F[对账完成]
E -->|否| G[记录差异]
G --> H[人工处理]6.2 差错处理
差错类型:
- 长款:第三方有,本地没有 → 补单
- 短款:本地有,第三方没有 → 查询第三方状态
- 金额不符:金额不一致 → 以第三方为准,调整本地
七、面试要点
7.1 常见追问
Q1: 如何保证支付幂等性?
A: Token机制 + 唯一键约束
- Token机制:获取Token → 提交时消费 → 重复请求Token已失效
- 唯一键约束:trade_no UNIQUE,重复插入会报错
Q2: 账户模型如何设计?
A: 借贷记账法
- 账户表:记录余额
- 流水表:记录每笔交易(借贷必相等)
- 校验:Sum(借方) + Sum(贷方) = 0
Q3: 如何保证资金安全?
A: 多重保障:
- 数据库事务ACID
- 乐观锁(版本号)
- 流水记录(可追溯)
- 定时对账(发现差异)
Q4: 对账系统如何设计?
A:
- 定时获取第三方对账单
- 对比本地订单
- 记录差异订单
- 人工处理差错
7.2 扩展知识点
相关场景题:
八、总结
支付系统设计要点:
- 幂等性:Token机制 + 唯一键约束
- 账户模型:借贷记账法
- 资金安全:事务 + 乐观锁 + 流水
- 对账系统:定时对账 + 差错处理
面试重点:
- 能画出完整的支付流程图
- 能解释幂等Token机制
- 能说出借贷记账法原理
- 能设计对账系统
参考资料:
- 支付宝架构演进
- 微信支付技术分享
