ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

面试官:支付系统如何防止重复支付?

面试官:支付系统如何防止重复支付? 每个支付系统都要回答一个问题用户点了两次怎么保证只扣一次钱大多数工程师听到这个问题第一反应是“用幂等键Idempotency Key。”这个回答没错但远远不够。在实际面试中提到幂等键通常只是讨论的起点——真正的较量在接下来的追问中才会展开。本文通过一次面试对话来呈现一个生产级幂等方案的设计推演过程。问题定义不确定性才是根源面试官提出的场景很经典用户在支付页面点击“立即支付”网关成功扣款但响应超时。用户不知道结果再次点击。真正的痛点不是重复点击而是不确定性。客户端不知道支付是否成功重试是必然的。我们要解决的问题是让重试变得安全。第一层方案幂等键最直接的想法为每次支付请求生成一个唯一标识服务端用这个键来识别重试。键由谁生成怎么生成有两种常见策略客户端生成 UUIDidempotencyKey:uuid.New().String()// 每个支付尝试生成一个重试时复用从业务属性派生raw:fmt.Sprintf(%s:%s:%d:%s,customerID,orderID,amount,currency,)idempotencyKey:fmt.Sprintf(%x,sha256.Sum256([]byte(raw)))后者看起来更“聪明”但有个问题业务语义会变得复杂。比如同一个订单五分钟后换了支付方式重试——这算同一笔尝试还是新的尝试不同的业务有不同的答案。一旦幂等键从业务属性派生防重逻辑就和业务规则紧密耦合了。成熟支付系统的做法是分离两个概念业务标识如订单号代表“为什么支付”幂等键代表“这一次执行尝试”。两者各司其职。原子性操作避免竞态条件一个常见的错误实现// 有竞态条件的写法if!exists(key){processPayment()save(key)}两个并发请求可能同时发现 key 不存在然后都执行扣款。正确的做法是依赖数据库的唯一约束做原子插入// 原子插入只有一个能成功_,err:db.ExecContext(ctx,INSERT INTO payment_requests (idempotency_key, order_id, status) VALUES ($1, $2, $3),key,orderID,PROCESSING,)iferr!nil{// 重复键错误 → 这是重试请求returngetExistingResult(ctx,key)}// 插入成功 → 处理支付processPaymentAndUpdate(ctx,key)关键设计点数据库唯一约束是防止并发重复的可靠防线应用层的检查-然后-插入总是存在竞态窗口。双重保护业务唯一索引如果客户端因为 bug 给同一个订单生成了不同的幂等键呢第一次OrderIdORD-1001, IdempotencyKeyabc123 第二次OrderIdORD-1001, IdempotencyKeyxyz789从幂等系统的角度看这是两个不同的操作。解决方案是再加一层保护——业务级别的唯一约束CREATE UNIQUE INDEX idx_payments_order_id ONpayments(order_id);这样即使幂等键不同同一个订单也不会被扣两次款。多个保护层分别应对不同的故障模式没有单一机制是充分的。服务崩溃场景网关交易参考号最棘手的场景支付网关成功扣款但我们的服务在保存结果前崩溃了。客户端重试时幂等表里没有记录系统会再次尝试扣款。解决方案与支付网关通信时带上商户交易号Merchant Transaction IDtypePaymentRequeststruct{MerchantTxIDstringjson:merchant_tx_id// ORDER-1001Amountint64json:amount// ...}网关会记录这个 ID。如果服务崩溃后重试网关收到相同的商户交易号会返回已有结果而不是再次扣款。这构成了第二道防线——即使我们自己的系统丢失了状态外部依赖仍然能提供保护。分布式事件让消费者也幂等在微服务架构中支付成功后会发布事件如OrderPaidEvent。Kafka 可能重复投递同一事件下游消费者必须自己实现幂等保护func(h*RewardHandler)Handle(ctx context.Context,event OrderPaidEvent)error{_,err:h.db.ExecContext(ctx,INSERT INTO rewards (order_id, amount) VALUES ($1, $2) ON CONFLICT DO NOTHING,event.OrderID,event.Amount,)returnerr// 冲突时返回 nil不报错}幂等性必须贯穿整个事件链在每一层都主动防范重复不能依赖上层传递的“只处理一次”标记。总结幂等性的三层防护完整的方案是多层防护的组合幂等键客户端生成请求级别防重依赖数据库唯一约束保证原子性业务唯一索引订单级别防重即使幂等键变化也能拦截重复支付网关交易参考号外部依赖保护服务崩溃后重试时避免二次扣款这三层各自应对不同的故障模式相互补充而非替代。最终目标不是阻止重试而是确保无论多少次重试用户只被扣一次钱。在分布式系统中“恰好一次”Exactly Once不是字面意义上的只执行一次而是重复的请求产生相同的结果。
返回列表