定期生成并发送月账单


对于一个国际化应用,生成月账单时通常以美金计算,但是发送给用户的时候需要按照本地货币单位展示。

月账单生成任务类

我们在推送账单生成任务到队列时,传入用户实例和月份(为什么在这里传入月份而不是任务执行时自动获取?结合上篇教程思考下):

生成月账单的 GenerateInvoice 任务类代码如下(重点关注 handle 方法即可):

首先获取通过美元结算的金额,然后通过汇率换算将其转化为本地货币对应的金额值,最后加上税得到总的账单金额,发送账单通知给用户。

在这里,货币换算汇率调用了 API 请求动态获取,因为货币汇率都是实时变动的,然后将其保存到静态数组中以便在当前任务处理期间加快后续获取速度(这种技术称之为 Memoization,详见递归性能优化教程):

不过这种优化也会引入一个新的问题,我们下面就来介绍。

静态数组的问题

月账单生成和发送任务第一个月可以很好地工作,但是第二个月就出问题了,因为使用的汇率还是上个月的,即使是全新推送的任务,也是这样的,因为 Currency 保存了汇率到本地。

这种情况本质上是因为当我们启动一个新的队列处理进程时,会开启一个新的控制台应用实例,这个实例是常驻内存的,除非崩溃或者重启,否则它一直使用的就是启动时的状态,同样,Curreny::$rates 作为一个静态属性一旦设置也会常驻内存,所以下个月、下下个月,只要不重启进程,使用的都是第一个月存储的汇率。

这个和处理 Web 请求不同,后者每次新的请求都会重新启动一个新的 Laravel HTTP 应用实例,这样每次新请求会重新请求 ExchangeRateApi 获取最新汇率。

让任务类自给自足

和上篇教程一样,要解决这个问题,需要让任务类做到「自给自足」,也就是任务被推送到队列的那一刻,就已经包含执行时需要的所有数据了。

要做到这一点不难,就是在推送任务时,将最新汇率作为参数传递过去:

如果推送任务是通过 CRON 任务调度完成的,每次调度 CRON 任务时,会启动一个全新的控制台应用,所以这个时候也会重新请求 ExchangeRateApi 获取最新汇率。

调整 GenerateInvoice 类的 handle 方法中如下:

这样一来,每次生成月账单时,使用的都是从外部动态传入的最新汇率。

每个任务使用新状态

为什么队列处理通过单个进程来做呢,这是为了提升性能,因为每次新的 Laravel 应用启动都要涉及服务提供者、扩展包、容器绑定等一大堆初始化操作,使用一个独立的常驻内存进程可以让队列消费更快、负载更低。

如果你确实想要每次启动新的 Laravel 应用实例,也不是不可以,使用 queue:listen 命令即可:

该命令也会启动一个常驻内存的进程,但是和 queue:work 不同的是,每次有新的队列任务需要处理时,会新建一个子进程来处理它,这个时候,就是一个全新的 Laravel 应用了。

不过,这个性能肯定是不及 queue:work 的,在生产环境应该尽量避免使用它。


Vote Vote Cancel Collect Collect Cancel

<< 上一篇: 业务流量峰值检测和数据一致性

>> 下一篇: 处理有访问频率限制的 API 请求