Featured image of post Laravel Queue Job 在 Transaction 內讀到舊資料的解法

Laravel Queue Job 在 Transaction 內讀到舊資料的解法

Transaction 內 dispatch 的 Job 因 SerializesModels 讀到舊資料,用 afterCommit() 讓 Job 等 commit 後再入列。

在 Transaction 裡 dispatch Job 到 Queue,Job 執行時讀到的竟然是舊資料。

為什麼 Job 拿到舊資料

Transaction 與 Queue Job 的時序競爭

先準備環境:Laravel 連接真實資料庫、Queue Driver 使用 Redis、資料庫裡已有一筆 User、執行 php artisan queue:work

以下程式碼中,Job 延遲 3 秒執行,Transaction 在 5 秒後才 commit:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// tests/Feature/ExampleTest.php
namespace Tests\Feature;

use App\Jobs\EmailChanged;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    public function test_dispatch_user_email_changed(): void
    {
        DB::transaction(static function () {
            $user = User::findOrFail(1);
            $oldEmail = $user->email;
            $newEmail = 'test'.random_int(1, 100).'@gmail.com';
            $user->fill(['email' => $newEmail])->save();
            EmailChanged::dispatch($user, $oldEmail, $newEmail)
                ->delay(now()->addSeconds(3));
            sleep(5);
        });
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// app/Jobs/EmailChanged.php
namespace App\Jobs;

use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class EmailChanged implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
    private User $user;
    private string $oldEmail;
    private string $newEmail;
    public function __construct(User $user, string $oldEmail, string $newEmail)
    {
        $this->user = $user;
        $this->oldEmail = $oldEmail;
        $this->newEmail = $newEmail;
    }
    public function handle(): void
    {
        dump('old email: '.$this->oldEmail);
        dump('new email: '.$this->newEmail);
        dump('current email:'.$this->user->email);
    }
}

執行後會發現 current email 還是 old email。因為 SerializesModels 只存 Model ID,Job 執行時會重新從 DB 撈資料,但這時 Transaction 還沒 commit,所以讀到的是舊值。

加上 afterCommit

只要在 dispatch 時加上 afterCommit(),Laravel 就會等 Transaction commit 後才真正把 Job 送進 Queue:

1
2
3
EmailChanged::dispatch($user, $oldEmail, $newEmail)
    ->delay(now()->addSeconds(3))
    ->afterCommit();

這樣 Job 執行時就能讀到正確的新資料了。