面试实录 —— 电商支付系统中,如何有效避免用户重复支付?

10 小时 17 分钟前
 YunFun

面试官:在电商支付系统中,如何有效避免用户重复支付?请详细阐述你的设计思路。

应试者:防止重复支付是电商支付系统的核心设计挑战之一。我的解决方案主要从以下几个维度考虑:

唯一性标识设计

在支付流程中,我们需要为每笔交易生成全局唯一且可追溯的幂等标识:

type PaymentIdentifier struct {
    OrderID       string    // 订单 ID
    UserID        int64     // 用户 ID
    TransactionID string    // 全局唯一事务 ID
    CreatedAt     time.Time // 创建时间
}

// 生成全局唯一事务 ID
func GenerateTransactionID() string {
    // 使用雪花算法生成分布式唯一 ID
    return snowflake.Generate().String()
}

幂等性控制机制

核心实现思路:

type PaymentService struct {
    // 分布式锁,防止并发冲突
    locker distributed.Locker
    
    // 已处理交易的缓存
    processedTransactions *sync.Map
    
    // 数据库连接
    db *gorm.DB
}

func (s *PaymentService) ProcessPayment(ctx context.Context, payment *Payment) error {
    // 1. 获取分布式锁
    lock, err := s.locker.Lock(payment.TransactionID)
    if err != nil {
        return errors.Wrap(err, "获取锁失败")
    }
    defer lock.Unlock()
    
    // 2. 检查交易是否已处理
    if _, processed := s.processedTransactions.Load(payment.TransactionID); processed {
        return errors.New("交易已处理")
    }
    
    // 3. 数据库层面的幂等性检查
    var existingPayment Payment
    if err := s.db.Where("transaction_id = ?", payment.TransactionID).First(&existingPayment).Error; err == nil {
        return errors.New("重复交易")
    }
    
    // 4. 执行支付逻辑
    if err := s.executePayment(payment); err != nil {
        return err
    }
    
    // 5. 记录已处理交易
    s.processedTransactions.Store(payment.TransactionID, true)
    
    return nil
}

多层幂等性保障

面试官:能详细解释一下你提到的多层幂等性保障吗?每一层具体是如何实现的?

应试者:多层幂等性保障是一种分层防重复提交的策略:

客户端层

type PaymentRequest struct {
    RequestID     string    // 客户端生成的唯一请求 ID
    OrderID       string
    Amount        decimal.Decimal
    PaymentMethod string
}

func GenerateClientRequestID() string {
    // 结合时间戳、随机数、设备 ID 等
    return fmt.Sprintf("%s-%d-%s", 
        time.Now().Format("20060102150405"),
        rand.Int63(),
        deviceID)
}

网关层限流与去重

type PaymentGateway struct {
    // 使用 Redis 实现请求去重
    requestCache *redis.Client
    
    // 限流器
    rateLimiter *rate.Limiter
}

func (pg *PaymentGateway) ValidateRequest(req *PaymentRequest) error {
    // 限流检查
    if !pg.rateLimiter.Allow() {
        return errors.New("请求过于频繁")
    }
    
    // 请求去重
    cacheKey := fmt.Sprintf("payment:request:%s", req.RequestID)
    
    // 使用分布式缓存防重
    if pg.requestCache.Exists(cacheKey).Val() > 0 {
        return errors.New("重复请求")
    }
    
    // 缓存请求,设置过期时间
    pg.requestCache.Set(cacheKey, "1", time.Minute*10)
    
    return nil
}

服务端事务管理

func (s *PaymentService) ProcessPayment(ctx context.Context, req *PaymentRequest) error {
    // 开启数据库事务
    tx := s.db.Begin()
    
    // 检查是否存在相同的事务
    var existingTxn PaymentTransaction
    if err := tx.Where("request_id = ?", req.RequestID).First(&existingTxn).Error; err == nil {
        tx.Rollback()
        return errors.New("事务已存在")
    }
    
    // 创建新的支付事务
    txn := PaymentTransaction{
        RequestID:  req.RequestID,
        Status:     "Processing",
        CreateTime: time.Now(),
    }
    
    if err := tx.Create(&txn).Error; err != nil {
        tx.Rollback()
        return err
    }
    
    // 执行实际支付
    if err := s.executePayment(tx, req); err != nil {
        tx.Rollback()
        return err
    }
    
    // 提交事务
    return tx.Commit().Error
}

数据库唯一约束

-- 创建支付事务表
CREATE TABLE payment_transactions (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    request_id VARCHAR(64) UNIQUE NOT NULL,  -- 唯一约束
    order_id VARCHAR(64) NOT NULL,
    status ENUM('Processing', 'Success', 'Failed') NOT NULL,
    amount DECIMAL(10,2) NOT NULL,
    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

这种多层设计的优势:

面试官:如果在高并发场景下,这套机制可能会引入性能瓶颈,你有什么优化建议吗?

应试者:高并发场景下的性能优化是一个非常关键的话题。我的优化建议包括:

缓存优化

异步处理

func (s *PaymentService) AsyncPaymentProcess(req *PaymentRequest) {
    // 使用消息队列异步处理支付请求
    go func() {
        // 投递到消息队列
        err := s.messageQueue.Publish("payment_topic", req)
        if err != nil {
            // 记录投递失败日志
            log.Error("消息投递失败", err)
        }
    }()
}

// 消息消费者
func (s *PaymentService) PaymentConsumer() {
    for {
        msg := s.messageQueue.Consume("payment_topic")
        
        // 并发处理
        go s.ProcessPayment(context.Background(), msg)
    }
}

细粒度锁

type ConcurrentPaymentManager struct {
    // 使用分段锁减少锁竞争
    shardedLocks []*sync.RWMutex
}

func (m *ConcurrentPaymentManager) getLock(key string) *sync.RWMutex {
    // 对 key 进行哈希,选择锁
    return m.shardedLocks[hashCode(key) % len(m.shardedLocks)]
}

func (m *ConcurrentPaymentManager) ProcessPayment(req *PaymentRequest) {
    lock := m.getLock(req.RequestID)
    lock.Lock()
    defer lock.Unlock()
    
    // 处理支付逻辑
}

限流与熔断

type AdaptiveRateLimiter struct {
    // 动态调整的令牌桶
    limit *rate.Limiter
}

func (rl *AdaptiveRateLimiter) Adjust(currentLoad float64) {
    // 根据系统负载动态调整限流阈值
    if currentLoad > 0.8 {
        rl.limit = rate.NewLimiter(rate.Limit(50), 100)
    } else {
        rl.limit = rate.NewLimiter(rate.Limit(100), 200)
    }
}

监控与性能分析

面试官:最后,对于这样一个支付系统,你有什么架构 level 的思考?

应试者:支付系统不仅仅是技术实现,更是一个复杂的金融级系统。我的架构思考主要包括:

安全性

可用性

合规性

可观测性

核心是在高性能、高可用、安全性之间找到平衡,构建一个既健壮又灵活的支付系统架构。


更多 Go 高质量内容试读👇: https://portal.yunosphere.com

欢迎关注我,经常分享有用的 Go 知识 / 面试 / 创业经历 / 其他编程知识 👇

3536 次点击
所在节点    程序员
32 条回复
juzuojuzou
10 小时 10 分钟前
假如,用户多次唤起支付宝但未支付,然后在支付宝里面看到多笔待支付单,再从支付宝里面全部支付呢
YunFun
10 小时 5 分钟前
@juzuojuzou #1 不是,想尽办法支付是吧,这种用户给我来一打😂
lry
9 小时 45 分钟前
用户如果退出再重新进入页面,怎么保证幂等 ID ?
YunFun
9 小时 30 分钟前
@lry 理解支付是依赖订单的,幂等在订单维度先拦截,不重新下单的话没有支付的必要条件
wjfz
9 小时 12 分钟前
重复支付问题主要是分两步,第一是生成本系统订单,第二是拿本系统订单去支付。
1 、生成本系统订单,客户端点完提交按钮就置灰,服务端返回订单号,进入待支付页面。
2 、在待支付页面选择支付方式,提交支付,点完提交置灰,生成第三方支付平台的订单。跳转第三方平台去支付,支付完成跳回来,完成支付。

两次置灰能把绝大多数重复请求扼杀在摇篮里。第一步中万一生成了重复的本地订单,因为是带着订单号在本地跳转的,用户全流程都在最后一次(或者初次)生成的订单号流程里,另一个待支付订单只会停留在订单列表。这一步可以加点限流或者自动取消订单操作。
第二步中,如果用户取消支付,在同一个支付平台重新发起支付,第三方支付的订单号是可以复用的,即使重复点击,也不会生成重复的支付宝订单,就不会出现 1 楼说的问题。
再扩展一下,假如用户切换了微信支付,在收到支付成功后可以用本系统订单号取消其他支付宝待支付订单。

再回头看看文章,1 、让客户端生成 RequestID 没有太大意义,因为重复订单都是客户端自己提交上来的。2 、并发大的时候用异步想死,用户一定要迅速看到支付结果,如果异步处理订单,用户就有可能看到订单状态没变重复支付。
YunFun
9 小时 9 分钟前
@wjfz 点赞👍 我这里是提供个思路,具体场景还需要不断迭代优化
lscho
9 小时 4 分钟前
@juzuojuzou

同支付通道做支付信息缓存处理,多次唤起支付宝都是同一个支付单。
不同支付通道(比如先唤起支付宝不支付,再唤起微信),唤起第二个通道的时候同时取消第一个通道的交易。
YunFun
8 小时 58 分钟前
@lscho 有用过这个方案
PendingOni
8 小时 33 分钟前
重放攻击?
markgor
7 小时 49 分钟前
首先我不懂 GO ,但看了一下大致流程,可能和现实有出入吧。
1 、首先通过 订单 + 用户 产生一个雪花 ID (全局事务 ID )
2 、通过雪花 ID 进行控制幂等。
---------------如果我没理解错的话,上面这里就忽略了用户需要更改支付途径的场景了,
比如一开始选择微信支付,后来想换支付宝,此时按你这个设计,用户只能重新下单支付。


多层幂等性保障:
网关层限流有必要,去重毫无必要,只是徒增了系统负担,本来根据用户 ID 就能达到限流。至于去重,只需要前端一个 disable 就好了。PS:如果说开控制台修改的话,那其实写脚本刷 token 然后去请求也一样,并且你这里根据客户端层生成 token 进行限流,实际已经没了限流的意义了(因为我可以刷出很多 token )。

服务端事务管理:
这里的 request_id 是一开始最上面 TransactionID 吗?如果不是没能理解。
janus77
7 小时 43 分钟前
@juzuojuzou #1 支付成功和订单结算完成应该是两个单独的模块,支付成功后可以再去操作重复的订单,保证结算成功的只有一个订单,如果用户不需要立马在界面上看到你的订单状态,这就可以延后处理;如果需要,那一般只展示一个订单,反正对于用户界面来说他看到的信息都一样,那就是一个订单,后端重复 id 这种东西对用户来说没有意义
qinxi
7 小时 32 分钟前
一个订单生成一个支付单.
切换支付方式取消前面的支付单.
剩下的人家支付系统已经实现好了 用电商端操心?
coderzhangsan
7 小时 31 分钟前
重复支付幂等性原则确立的目标不应该是业务数据吗?例如业务订单号,请求 id 在业务上没有太大的意义吧。

而实际支付时,用户多次拉起支付并不支付,因此当前业务订单会产生多笔业务交易流水,这个业务交易流水号也就是三方支付平台对应的商家(开发者)支付单号,所以一般不会用业务订单号作为商家(开发者)支付单号,除非用户每次下单都会创建新的订单。

根据上面得设计,重复支付应该在业务订单增加中间态校验和去重判断吧。
markgor
7 小时 31 分钟前
其实我觉得很简单的事情
1 、重复支付的问题,只需要执行退款即可,这个是个兜底策略。
2 、常规流程如下:
[前端]->发起预定请求->[后端]
*后端:生成订单返回单号;(没必要做幂等,就算抢购/优惠卷限购的场景,也是对应的方法进行判断而非订单服务主线程这里进行判断幂等。

[前端]->发送支付方式->[后端]
*后端:检查 流水表看是否有支付请求记录,如有则调用对应渠道支付检查,如发现支付成功返回给前端,如发现尚未支付成功则调用渠道的取消订单接口,把之前的支付订单取消掉。然后生成新的支付流水。
支付流水表:流水 ID + 订单 ID + 渠道 ID

[前端]->根据后端返回的支付信息,通过流水 ID 拉起支付->[支付平台]
[前端]->輪詢支付結果,或通過 ws/sse 獲取->[後端]

異步部分:[支付平台]->消息推送(关闭、支付)->[后端]
*后端根据流水 ID ,反查订单 ID ,然后通过支付平台接口二次确认支付结果,如结果一致,且之前尚未处理交易信息,则处理交易信息。如之前已经处理完了交易信息,则对本次建议信息进行退款操作,即一開始提到的兜底策略,這裡只需要保證流水 ID 是冪等即可。

上面應該還有一個是在於處理超時未支付訂單的流程,但這個主要看單量,不同單量不同處理方式,最簡單就直接定時取消,量大的就丟去隊列裡面消費,消費時候檢查支付結果即可。
layxy
7 小时 5 分钟前
@juzuojuzou 只要支付成功一笔,其他的订单应该调用支付渠道关闭订单
8355
6 小时 30 分钟前
有 2 个关键点我觉得你没明确说出来,面试官已经有合理引导 - 在架构层面的思考。

1.前端在同一个客户端需要保证同一时间仅发起一次请求,类似按钮禁用。
你的回答是幂等,幂等只是你接口需要达到的标准而不是措施或者方案,实际上是需要沟通前端来做,创建订单接口响应或达到超时时间前不能再次发起接口调用,那么理论上产生并发的情况就是多客户端并发下单,这种太极端了正常操作下是不存在的,并发锁拦截即可。这里考的是你是不是只考虑自己的代码而忽略了项目架构。

2.支付平台层级是可以保证同一个订单号绝对不会出现重复支付的情况。
那么实际上后端只需要保证不会并发生成订单号即可,增加简单的并发锁就行了。

在实际的业务中,可能存在非常多的关联业务系统,例如风控系统(可以拦截/取消订单之类的兜底方案和策略)等,实际上是没办法设计你这么复杂的方案去实现这个需求的。
linxb
6 小时 5 分钟前
重复支付的话,给用户退款不就好了
ffw5b7
6 小时 4 分钟前
有一个问题想咨询下,分布式锁是第三方组件实现的对吧,如果是集群的,存在数据延迟同步怎么解决?
zsc8917zsc
6 小时 4 分钟前
闲鱼上的那个快递费,好像是支付宝家的,他家就有重复支付的问题,但是过一段时间后会自动退费,亲身经历。所以...避免啥啊避免,有策略兜底就完事了。
jonsmith
5 小时 41 分钟前
这解决不了重复支付吧,一般在创建支付前先调用第三方支付,查下上笔支付的状态。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1094298

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX