取消会议和自动退款处理


2020 年,因为新冠疫情原因很多行业交流会议被取消,这就涉及到如何处理退款,理想的场景是当我们在系统管理界面点击取消按钮时,就会在后台自动批量处理所有已付款用户的退款,然后给对应用户发送会议取消和退款成功通知。

编写任务类

对于这样的耗时操作,显然可以借助消息队列来异步处理。我们创建一个对应的会议取消任务类 CancelConference,在 handle 方法中,我们在 each 方法的匿名函数中为每一个参会者处理退款,并发送邮件通知:

处理队列任务超时

这里面,如果使用了第三方支付服务的话,退款需要涉及到网络请求,邮件发送也是个耗时操作,而 Laravel 任务类默认的超时时间是 60s,可能不够,你可以指定 --timeout 将其延长:

这里我们将其设置为了 300 分钟,也就是 5 个小时,之所以设置这么长,是因为 CancelConference 会在一次执行期间为所有参会者退款并发送通知。

当然,和 tries--backoff 等参数一样,你也可以通过任务类的属性设置超时时间:

推荐这样做,因为只有这个任务需要设置很长的超时时间,其他任务不需要这么长。

避免队列任务重复执行

从队列中获取任务时,任务被标记为 reserved,这样就不能被其他处理器获取了,当任务执行完成后,要么从队列中移除,要么再次被推送到队列重试。

但是如果队列任务在执行过程中队列处理器进程崩溃导致任务执行中断,则永远被标记为 reserved,不会再被执行。

要避免这个问题,Laravel 为 reserved 状态设置了超时时间,默认是 90s,可以在 queue.php 配置文件中配置这个值:

由于我们为 CancelConference 任务类设置了超时时间为 5 个小时,这会导致的一个问题是 90s 后,reserved 状态失效,其他处理器可以获取这个任务类执行,出现同一个任务的重复执行。

要解决这个问题,需要确保 retry_after 的值大于所有任务类的超时时间:

确保拥有足够多的处理器

如果一个任务需要执行 5 个小时,只有一个队列处理器进程的话,其他队列任务都会被堵塞,为此需要启动多个处理器进程,但是也有可能所有处理器进程同时都在执行这些耗时 5 小时的任务,一个解决办法是将这些任务推送到独立的队列:

并为其分配足够多的处理器进程:

使用独立的连接处理耗时任务

按住葫芦起了瓢,只有 1 个队列任务耗时 5 小时,却要连带所有其他任务的 reversed 状态需要 5 个多小时才能过期,这么做是不合适的,要解决这个问题,可以为该队列设置独立的连接:

然后在推送非常耗时的任务到队列的时候指定连接:

或者在任务类中通过属性值设置这个连接:

最后我们还可以在启动处理器进程的时候指定队列连接:

php artisan queue:work database-cancelations

指定连接的队列处理器进程只能消费该连接中的队列任务。

将单任务拆分为多个子任务

我们的 CancelConference 任务类可能需要运行长达 5 个小时才能完成所有退款和邮件发送,在此期间这个队列处理器进程只能处理这一个任务,如果在执行过程中出现异常,又要从头开始执行,并且跳过已经处理过的退款,这里面存在的不可控和风险比较大。

为此,我们可以将这个耗时很长的单任务分发拆分为多个子任务的分发,实现起来也不难,就是把原来一个任务处理所有退款和通知的实现代码:

调整为一个任务只处理一个用户的退款和通知,我们新建一个 RefundAttendee 任务类来处理这个工作,从外部传入 RefundAttendee 的就不再是 Conference 实例,而是 Attendee 实例了:

这样一来,我们就可以恢复到使用单个队列连接,也不需要配置那些额外的超时字段了:

这里,我们实际上是在一个队列任务(CancelConference)中将另一个队列任务(RefundAttendee)推送到消息队列进行处理。


Vote Vote Cancel Collect Collect Cancel

<< 上一篇: 发送 Webhook 实现跨应用异步回调

>> 下一篇: 通过幂等设计和原子锁避免重复退款