幂等性设计
RefundAttendee
tries
RefundAttendeehandle
双重检查
需要注意的是,调用第三方支付服务 API 进行退款会发起网络请求,如果退款成功,但是由于网络问题导致返回响应失败,那么永远也不会将订单标记为已退款。同样的问题也肯能会出现在退款后更新订单退款状态时。
这种情况下,只有第三方支付服务提供商才是唯一可信的数据源,只有他们知道是否完成了退款操作。因此,我们需要基于支付服务商提供的 API 来判断是否退款成功,而不是本地数据库字段:
这一点在开发与第三方服务 API 交互的功能时非常重要,也是最容易出问题,但又不方便排查的地方。
refundwasRefunded
三重检查与原子锁
RefundAttendee
如果两个队列处理器进程同时获取到这两个执行同一笔退款的任务,可能会存在同时发送 HTTP 请求并进行退款的操作,进而出现并发安全问题:
要避免这个问题,可以使用原子锁来保证每笔退款操作串行执行:
refund.{id}
任务执行完成后,会自动释放这个锁。
这里我们使用了基于缓存的原子锁,这是 Laravel 底层提供的功能,除此之外,还有基于 Redis、数据库的实现版本,具体可以参考学院君之前发布的基于 Redis 实现分布式锁及其在 Laravel 底层的实现源码 这篇教程。
通过锁管理重试
RefundAttendee
要解决这个问题,需要设置原子锁的过期时间并确保下次重试的时候锁已经释放:
Cache::lock
由于处理器进程崩溃后,下次重试是在 90s 之后(参考上篇教程避免队列任务重复执行中的介绍),此时锁已过期,就会重新获取到一把新锁来执行这个任务。
配置任务延迟
如果是任务执行期间出现异常,锁会自动释放,但是如果释放锁的时候出问题咋整?
如果任务执行失败,会在 11s 之后执行,这样,就可以确保即便锁释放失败,也能正常执行这个任务了。
注意:如果队列任务执行失败,11s 后重试,如果是队列处理器进程崩溃,则是 90s 后重试,这两个重试时间是不一样的。