Laravel框架中在数据库事务中调度队列方式浅析

2023-06-01 00:00:00 队列 调度 浅析

每当您在事务中处理队列时,您都需要注意一个 “陷阱”。

为了提供一些上下文,让我们继续使用之前的代码示例。

我们可以想象,在我们创建了我们的用户之后,我们想要运行一个任务来提醒管理员通知他们新注册并向新用户发送欢迎电子邮件。

我们将通过分派一个名为 AlertNewUser 的队列任务来做到这一点,如下所示:

use Illuminate\Support\Facades\DB;
use App\Jobs\AlertNewUser;
use App\Services\ThirdPartyService;

DB::transaction(function () use ($user, $request): void {
    $user = User::create([
        'email' => $request->email,
    ]);
   
    $user->roles()->attach(Role::where('name', 'general')->first());
   
    AlertNewUser::dispatch($user);
});

当您开始一个事务并对其中的任何数据进行更改时,这些更改仅对正在运行事务的请求 / 进程可用。

对于任何其他访问您更改的数据的请求或进程,必须先提交事务。

因此,这意味着如果我们从事务内部分派任何排队的队列、事件监听器、邮件,通知或广播事件。

由于竞争条件,我们的数据更改可能在事务内部不可用。


如果队列在事务提交之前开始处理排队的代码,就会发生这种情况。

因此,这可能导致您的排队代码可能试图访问不存在的数据,并可能导致错误。在我们的例子中,如果在事务提交之前运行队列 AlertNewUser 作业,那么我们的作业将尝试访问一个尚未实际存储在数据库中的用户。

如您所料,这将导致作业失败。

为了防止这种竞争条件的发生,我们可以对我们的代码和 / 或我们的配置进行一些更改,以确保仅在事务成功提交后才调度队列。


我们可以更新 config/queue.php 并添加 after commit 字段。

让我们想象一下,我们正在使用 redis 队列驱动程序,所以我们可以这样更新配置:

<?php
return [
    // ...
    'connections' => [
        // ...
        'redis' => [
            'driver' => 'redis',
            // ...
            'after_commit' => true,
        ],
        // ...
    ],
    // ...
];

通过进行此更改,如果我们尝试在事务内调度队列,则队列将在实际调度队列之前等待事务提交。 

方便的是,如果事务回滚,它也会阻止队列被调度。


然而,可能有一个原因,您不希望在配置中全局设置此选项。 

如果是这种情况,Laravel 仍然提供了一些很好的助手方法,我们可以根据具体情况使用它们。

如果我们想更新事务中的代码,只在任务提交后才分派任务,可以使用 afterCommit() 方法,如下所示:

use Illuminate\Support\Facades\DB;
use App\Jobs\AlertNewUser;
use App\Services\ThirdPartyService;

DB::transaction(function () use ($user, $request): void {
    $user = User::create([
        'email' => $request->email,
    ]);
   
    $user->roles()->attach(Role::where('name', 'general')->first());
   
    AlertNewUser::dispatch($user)->afterCommit();
});

Laravel 还提供了另一个我们可以使用的方便的 beforeCommit() 方法。

如果我们在队列配置中设置了全局 after_commit => true,但不关心等待事务被提交,就可以使用这个。要做到这一点,我们可以简单地像这样更新我们的代码:

use Illuminate\Support\Facades\DB;
use App\Jobs\AlertNewUser;
use App\Services\ThirdPartyService;

DB::transaction(function () use ($user, $request): void {
    $user = User::create([
        'email' => $request->email,
    ]);
   
    $user->roles()->attach(Role::where('name', 'general')->first());
   
    AlertNewUser::dispatch($user)->beforeCommit();
});

相关文章