Laravel 多租户应用中的单个共享队列工作者

Single shared queue worker in Laravel multi-tenant app

我正在构建一个多租户 Laravel 应用程序(在 Laravel 5.3 上),它允许每个租户针对任何受支持的 Laravel 设置拥有自己的一组配置。目前,这是通过使用我自己提供自定义配置加载器(覆盖默认值 Illuminate\Foundation\Bootstrap\LoadConfiguration)的实现覆盖默认值 Laravel Application 来实现的。应用程序从 bootstrap 上的环境(PHP 的 $_ENV.env 文件)检测当前租户,然后为检测到的租户加载适当的配置文件。

上述方法对 HTTP 和控制台内核都非常有效,其中每个内核 request/command 都有有限的生命周期,但我不确定如何处理队列工作程序。我希望所有租户都有一个队列工作人员,并且我已经实现了一个自定义队列连接器,以便在安排队列作业时添加额外的元数据,以便在工作人员收到租户时识别租户。

我寻求您帮助的部分是如何 运行 在隔离环境中对每个队列作业进行排序,我可以 bootstrap 使用我的自定义配置。

我看到的一些可能的解决方案是:

重要的是,我希望这个多租户作业的执行对作业本身是透明的,这样在多租户环境中没有设计为 运行 的作业(例如来自Laravel Scout 等第三方包无需任何修改即可处理。

关于如何解决这个问题有什么建议吗?

我们的情况差不多。这是我们的方法:

服务提供商

我们有一个名为 BootTenantServiceProvider 的 ServiceProvider,它在正常的 HTTP/Console 请求中引导租户。它期望存在一个名为 TENANT_ID 的环境变量。这样,它将加载所有适当的配置并设置特定的租户。

具有 __sleep() 和 __wakeup()

的特征

我们有一个 BootsTenant 特征,我们将在我们的队列作业中使用它,它看起来像这样:

trait BootsTenant
{
    protected $tenantId;

    /**
     * Prepare the instance for serialization.
     *
     * @return array
     */
    public function __sleep()
    {
        $this->tenantId = env('TENANT_ID');

        return array_keys(get_object_vars($this));
    }

    /**
     * Restore the ENV, and run the service provider
     */
    public function __wakeup()
    {
        // We need to set the TENANT_ID env, and also force the BootTenantServiceProvider again

        \Dotenv::makeMutable();
        \Dotenv::setEnvironmentVariable('TENANT_ID', this->tenantId);

        app()->register(BootTenantServiceProvider::class, [], true);
    }
}

现在我们可以编写一个使用此特征的队列作业。当作业在队列上序列化时,__sleep() 方法将在本地存储 tenantId。反序列化时,__wakeup() 方法将恢复环境变量和 运行 服务提供商。

队列作业

我们的队列作业只需要使用这个特性:

class MyJob implements SelfHandling, ShouldQueue {
    use BootsTenant;

    protected $userId;

    public function __construct($userId)
    {
        $this->userId = $userId;
    }

    public function handle()
    {
        // At this point the job has been unserialized from the queue,
        // the trait __wakeup() method has restored the TENANT_ID
        // and the service provider has set us all up!

        $user = User::find($this->userId);
        // Do something with $user
    }
}

与 SerializesModels 冲突

Laravel 包含的 SerializesModels 特性提供了自己的 __sleep__wakeup 方法。我还没有完全弄清楚如何让这两个特性一起工作,或者即使这是可能的。

现在我确保我从不在构造函数中提供完整的 Eloquent 模型。您可以在上面的示例作业中看到,我只将 ID 存储为 class 属性,而不是完整模型。我有 handle() 方法在队列 运行 时间内获取模型。那么我根本不需要 SerializesModels 特性。

使用queue:listen代替--daemon

您需要 运行 您的队列工作人员使用 queue:listen 而不是 queue:work --daemon。前者为每个队列作业启动框架,后者将启动的框架加载到内存中。

至少,假设您的租户启动过程需要全新的框架启动,您需要执行此操作。如果您能够连续启动多个租户,干净地覆盖每个租户的配置,那么您可能能够逃脱 queue:work --daemon 就好了。

要扩展@jszobody 他的答案,请参阅 TenantAwareJob trait build by multi-tenant Laravel package。

这完全符合您的要求,在睡眠之前对您的租户进行编码,在醒来时启动您的租户。

此特性也适用于 SerializesModels,因此您可以传递您的模型。

更新

因为 Laravel 6 这不再有效。 SerializeModels 特征覆盖 __serialize__unserialize functions.

新方法是注册服务提供者并挂接到队列。通过这种方式,您可以在处理之前添加有效负载数据并启动 Tentant。示例:

    public function boot()
    {
        $this->app->extend('queue', function (QueueManager $queue) {
            // Store tenant key and identifier on job payload when a tenant is identified.
            $queue->createPayloadUsing(function () {
                return ['tenant_id' => TenantManager::getInstance()->getTenant()->id];
            });

            // Resolve any tenant related meta data on job and allow resolving of tenant.
            $queue->before(function (JobProcessing $jobProcessing) {
                $tenantId = $jobProcessing->job->payload()['tenant_id'];
                TenantManager::getInstance()->setTenantById($tenantId);
            });

            return $queue;
        });
    }

灵感来自 laravel-multitenancy and tanancy queue driver