Skip to content

如果一笔订单,用户在微信和支付宝同时支付,会怎么样? ​

  • 标签:后端、场景题

🏛 业务架构师 ​

用户在微信和支付宝同时支付同一笔订单(常见于收银台双开、页面卡顿重复发起),问题的本质是:渠道异步回调的并发竞态 + 资金不能收双份。我分三层回答。

第一层:状态机原子更新(只能成功一笔)

订单状态流转用条件更新串行化,回调处理前先原子抢占:

sql
UPDATE orders SET status='PAID', channel='WECHAT'
WHERE order_id=? AND status='WAIT_PAY';
  • 微信回调先到:抢占成功,订单已支付
  • 支付宝回调后到:affected rows = 0,说明已有渠道成单——这笔不能确认成功,必须自动原路退款,绝不能因为"用户自己发起的"就吞下第二笔

第二层:防重与幂等

  • 每个渠道的回调用支付流水号唯一索引保幂等,同一渠道重复回调直接返回成功(渠道要求返回成功否则无限重试)
  • 两个渠道是不同流水号,唯一索引挡不住跨渠道,所以主防线必须是订单状态机

第三层:对账兜底(真实世界的主防线)

异步网络环境下,回调丢失、延迟、乱序都真实存在。每日与两个渠道的对账文件核对:

  • 渠道成功 + 本地未支付 → 补单还是退款,按业务规则自动处理
  • 双渠道都成功 → 自动退款第二笔,生成资损预警工单

业务视角收尾:这个场景的业务目标不是"技术上杜绝并发"(做不到,回调时机不受我们控制),而是无论竞态怎么发生,用户资金结果正确且尽量无感:快的那个成交,慢的那个秒级退款并短信告知"该订单已通过微信支付,支付宝款项已退回"。处理得好是无感细节,处理不好就是客诉和监管投诉。

🏗 系统架构师 ​

这是典型的并发支付幂等问题。如果没做防护,最坏结果是:一笔订单被两个渠道各扣一次款,订单状态被覆盖,对账时两边流水对不上,资损+客诉。

根因:支付回调到达时先改订单状态再通知渠道,两个渠道的回调没有互斥,也不校验订单当前状态。

我的方案核心是"一个订单同一时刻只允许一笔支付有效",分层防:

  1. 收单侧防并发:支付成功回调处理前,先执行原子抢占——UPDATE orders SET pay_status=paid, pay_channel=? WHERE order_no=? AND pay_status=unpaid,利用行锁 + 状态机条件更新,只有一方能更新成功, affected rows = 0 的那笔直接走原路退款。这是最后一道防线,必须有。

  2. 发起侧防重复:用户在微信发起支付时,把订单与渠道绑定(Redis SETNX order_no:channel + 短 TTL),另一渠道发起时要么拦截提示"已有支付进行中",要么先关闭上一笔再放行。微信、支付宝都有关单接口,切渠道前先调关单。

  3. 回调处理幂等:回调可能重复推送,处理逻辑以第三方流水号做幂等键,落一张支付流水表加唯一索引,重复回调直接返回成功。

  4. 兜底对账:每日与微信/支付宝侧对账文件核对,发现单边账自动触发退款或补单,并有运营介入入口。

架构上我强调一点:防线不能只建在一层,收单抢占是数据库层的硬保障,即使 Redis 挂了、前置校验被绕过也不会真正双扣款。

💻 全栈开发专家 ​

结论:第二个渠道的回调会被状态机挡下,然后触发自动退款。前提是你把幂等和状态机做对了。

实现

  1. 订单状态条件更新(核心互斥):
sql
UPDATE orders SET status = 'PAID', pay_channel = 'WECHAT', trade_no = ?
WHERE order_no = ? AND status = 'PENDING';

微信先回调成功,状态变 PAID;支付宝回调到来时 AND status='PENDING' 不满足,影响行数 0。

  1. 回调处理分支:
java
int updated = orderMapper.markPaid(...); // 条件更新
if (updated == 1) {
    // 正常推进:发支付成功消息、扣库存
} else {
    Order o = orderMapper.findByNo(orderNo);
    if (o.isPaid() && !"ALIPAY".equals(o.getPayChannel())) {
        refundService.autoRefund(o, alipayTradeNo); // 渠道不一致 -> 原路退回
    }
    // 已是本渠道已支付(重复回调)-> 直接返回成功,别让渠道重推
}

要点

  • 回调幂等:渠道流水号(transaction_id)建唯一索引,重复推送直接吸收。
  • 退款是异步的,要记退款流水 + 定时对账,退款失败转人工。
  • 极端情况"用户同时扫了两个码都在输密码",本质就是并发的两个回调,上面的行锁 + 条件更新天然串行化,不需要额外分布式锁。

踩过的坑 双渠道支付的根因常是前端重复拉起支付,产品层也该限制:已有待支付单时复用同一收银台订单,从源头减少。