回答重点
这种情况在正常的业务场景中是有可能出现的,因为订单都会有定时取消的逻辑,比如10 分钟或者 15分钟,而用户刚好卡在这个时间点进行付款,此时就会出现两种情况:
- 用户支付成功,支付回调的那一刻支付单刚好还没取消,而等回调结束,取消支付单的事务提交,支付单取消。此时用户扣款了,但是对应的权益或资产没了。

- 用户支付成功,支付回调的那一刻支付单已经被取消。但此时用户已经扣款,东西却没了

可以看到,不论是哪种情况,其实都需要做一定的处理,不然用户肯定会来投诉!
这种场景无非就是支付单支付成功和取消两种状态的“争夺”,正常情况下,订单或者支付单都会有状态机的存在,在当前场景简单来说有以下两条路径:
- 待支付->支付中->支付成功
- 待支付->支付中->已取消
针对情况1,如果是支付回调取胜,此时的状态应该已从 支付中->支付成功
针对情况2,如果是取消支付单取胜,此时的状态应该已从 支付中->已取消
所以我们在修改支付单状态的时候,基于原始状态的判断,就可以做正常的处理,来看下 SOL应该就很清晰了:
# 支付成功
update pay_info set status = 'paySuccess' where orderNo = '1' and status = 'paying';
# 取消
update pay_info set status = 'cancel' where orderNo = '1' and status = 'paying';
重点就是我们加了 status='paying’这个条件,这就能保证情况只有一个能成功,另一个一定失败。这种其实就是乐观锁的方式
- 假设情况1成功了,此时用户已经成功付款,那么状态已经变为paySucces,取消的SQL必定执行失败,此时就让它失败,不需要做任何别的处理。

- 假设情况2成功了,此时订单已被取消,status已经变为 cancel,支付成功的SOL必定执行失败,这种情况下我们就需要做逆向处理,即给用户退款。订单被取消,用户的钱也被原路退回,这种处理也没任何问题

业务优化
针对订单超时业务,这里在业务上可以做一个小优化,你想想,用户付款前可能有点挣扎,然后在最后一刻终于下定决心进行付款,这时候却告知被退款了,用户很可能就不会再下单了。因此我们在页面上可以限时订单取消设置计时为 10分钟,但实际后端是延迟 11 分钟取消订单,这样就能避免这种情况的发生啦。
Redis 分布式锁实现
最后除了利用数据库处理,还可以使用分布式锁,对一笔订单加锁也能保证这笔订单正常的业务流转。每次进行取消订单或付款操作时,首先尝试获取订单的分布式锁,确保只有一个操作能修改订单状态。在分布式系统中,订单在取消的同时用户付款的竞态问题可以通过分布式锁来解决。以下是一个具体的、落地的方案,确保订单状态的可靠性,避免因并发导致状态冲突
订单取消流程:
- 超时触发取消订单
- 取消订单方法中先获取该订单的分布式锁。如果锁被其他操作持有(如付款),等待或抛出异常
- 若成功获取锁,检查订单状态是否已付款:
- 若订单未付款,将订单状态更新为“已取消”
- 若订单已付款,直接跳过这笔订单的处理。。
- 释放分布式锁,完成取消流程。
订单付款流程:
- 三方支付成功回调。
- 后端系统接收回调后,先获取该订单的分布式锁,如果锁被其他提作持有(如取消),等待或抛出异常(没有给三方响应成功,三方会重新发起回调)
- 若成功获取锁,检查订单状态是否为“待支付”:
- 若订单状态为“待支付”,继续执行扣款,并将订单状态更新为“已付款”。
- 若订单状态为“已取消”,则发起退款,并提示用户订单已取消,无法支付。
- 释放分布式锁,完成流程。