Laravel 排程 ( schedule ) 與工作 ( job ) 詳解

在前篇文章都說明如何建立高流量、Redis counter、Sqids、多網域的短網址程式範例,接下來細部解說在 Laravel 如何將點擊的短網址,先暫存在 Cache 裡,再透過 Schedule 來將它寫入 Redis Server ,最後發動 Queue 的 worker 寫入 DB 的資料表裡。

大致的步驟

PHP

/**
* 1. 在 RedirectController 控制器裡, 將點擊統計暫存(key: "short_url:{$shortUrl->id}:clicks" )
*/
Cache::increment("short_url:{$shortUrl->id}:clicks");

// 2. 啟動 schedule run, 讓 routes/console.php 裡的 schedule 背景長駐
執行: php artisan schedule:work
//派發點擊數據同步工作 Job:SyncShortUrlClicksJob! , 會寫入 redis server
SyncShortUrlClicksJob::dispatch();

// 3.啟動 worker , 讓 worker 背景長駐: 實際寫入 db 要執行 php 
執行: php artisan queue:work

步驟說明

Step01: 在 ShortUrlRedirectController 裡,將點擊的短網址,先暫存在 Cache 裡

PHP
/**
* 1. Redis 點擊統計(key: "short_url:{$shortUrl->id}:clicks" )
*/
class ShortUrlRedirectController extends Controller
{
   // 短網址重定向, 將物件 controller 當作為 function 使用
   public function __invoke(string $code)
  {
    ....
    Cache::increment("short_url:{$shortUrl->id}:clicks"); // 每次呼叫,'short_url:{$shortUrl->id}:clicks' 的值就加 1 。
    ...
  }
}

Laravel Cache::increment 的意思是增加快取中某個整數鍵(key)的數值,它是一個原子操作,常用於計數器(如文章瀏覽數、點讚數),如果該鍵不存在,它會先將其設為 0 再加 1(或增加的量),並且可以設定增加的數量。

這代表:

  • 將點擊的記錄暫存
  • short_urls.clicks 欄位 不會立刻寫 DB

Step02: 建立 schedule : ‘shorturl:sync-clicks’ 定時寫入 redis Server

PHP
// 同步短網址點擊次數到資料庫
// 每分鐘執行一次 SyncShortUrlClicks Job
//Schedule::job(new SyncShortUrlClicksJob)->everyMinute();
// 1. 定義 Artisan 指令
Artisan::command('shorturl:sync-clicks', function () {
    // 這裡可以直接派發 Job
    SyncShortUrlClicksJob::dispatch();
    $this->info('派發點擊數據同步工作 Job:SyncShortUrlClicksJob!');
})->purpose('同步短網址的點擊次數');

// 2. 設定排程:使用指令名稱呼叫
Schedule::command('shorturl:sync-clicks')->everyMinute();

指令 SyncShortUrlClicksJob::dispatch(); 會寫入 Redis 裡,讓計數增加

Step03: 啟動 schedule 指令: php artisan schedule:work ,讓 routes/console.php 裡的 schedule 背景長駐

Bash
PS W:\nginx\html\twingo> php artisan schedule:work

2025-12-20 15:00:01 Running ["artisan" shorturl:sync-clicks] ............... 840.20ms DONE
   "W:\nginx\php\php.exe" "artisan" shorturl:sync-clicks > "NUL" 2>&1
image 10

可以使用 php artisan schedule:list 來看排程的狀況

Bash
W:\nginx\html\twingo> php artisan schedule:list

 * * * * *  php artisan shorturl:sync-clicks ........... Next Due: 43 seconds from now 
image 11
image 12

Step04: 啟動 worker 指令 php artisan queue:work,讓 worker 背景長駐: 實際寫入 db 要執行 php

Bash
 W:\nginx\html\twingo> php artisan queue:work
  2025-12-20 14:48:06 App\Jobs\SyncShortUrlClicksJob ............................... RUNNING  
  2025-12-20 14:49:56 App\Jobs\SyncShortUrlClicksJob ........................... 1m 49s DONE
  2025-12-20 14:49:56 App\Jobs\SyncShortUrlClicksJob ............................... RUNNING
  2025-12-20 14:50:07 App\Jobs\SyncShortUrlClicksJob .............................. 11s DONE 
image 13

為什麼「Redis 裡有值,但 pull() 卻是 0」?

原因 1(最常見):queue / scheduler 用了「不同 cache store」

檢查方式(直接加 log)

RedirectController

PHP
logger('web cache store', [config('cache.default')]);

Job

PHP
logger('job cache store', [config('cache.default')]);

如果不一樣,100% 就是這個問題

解法(一定要做)

.env 檔

Bash
CACHE_DRIVER=redis
QUEUE_CONNECTION=redis

改完一定要:

Bash
php artisan config:clear
php artisan cache:clear
php artisan queue:restart

原因 2:Redis prefix 不一致(Web vs Queue)

Laravel Redis 預設會加 prefix:

Bash
prefix = laravel_database_

queue worker 若吃到不同 env / config cache,prefix 會不同。檢查 Redis 內實際 key

Bash
redis-cli
keys *

你可能會看到:

Bash
laravel_database_short_url:1:clicks

但程式實際在找的是:

Bash
short_url:1:clicks 

解法(推薦)

統一用 Cache,不要直接用 Redis facade

Bash
Cache::increment($key);
Cache::pull($key, 0);

❌ 不要混用:

Bash
Redis::incr()
Cache::pull()

原因 3:Job 其實根本沒執行

檢查 queue worker 是否在跑

Bash
php artisan queue:work

# 檢查 failed jobs
php artisan queue:failed

原因 4:Scheduler 沒跑(不是 bug)

有寫:

Bash
$schedule->job(new SyncShortUrlClicks)->everyMinute();

但只跑了:

Bash
php artisan serve

Scheduler 根本不會動

正確方式(擇一)

Bash
php artisan schedule:work

# 或 crontab:
* * * * * php artisan schedule:run

原因 5:測試時「立即 pull 掉了」

PHP
// pull() = get + delete
$clicks = Cache::pull($key, 0);

如果:

  • Job 跑過一次
  • 你再跑一次

第二次一定是 0(這是正常行為)

Schedule 排程的三種實作方式

方法一:定義 Command 並排程(最推薦)

這是最結構化的做法。先定義一個指令名稱,再將該名稱放入排程中。

PHP
use App\Jobs\SyncShortUrlClicks;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schedule;

// 1. 定義 Artisan 指令
Artisan::command('shorturl:sync-clicks', function () {
    // 這裡可以直接派發 Job
    SyncShortUrlClicks::dispatch();
    $this->info('點擊數據同步 Job 已派發!');
})->purpose('同步短網址的點擊次數');

// 2. 設定排程:使用指令名稱呼叫
Schedule::command('shorturl:sync-clicks')->everyMinute();

方法二:直接在排程中使用 Artisan::call

如果你不想定義指令名稱,只想在排程時間到時直接執行某個現有的 Artisan 指令,可以使用 Artisan::call 的閉包寫法:

PHP
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schedule;

Schedule::call(function () {
    Artisan::call('shorturl:sync-clicks'); // 呼叫已存在的指令
})->everyMinute();

注意:通常直接使用 Schedule::command('name') 會比 Schedule::call 內包 Artisan::call 更簡潔且易於追蹤日誌。

方法三:在 Command 閉包內執行邏輯(不透過 Job)

如果你的同步邏輯非常簡單,甚至不需要建立一個 Job 類別,可以直接寫在 routes/console.php

PHP
Artisan::command('shorturl:sync-quick', function () {
    // 直接在這邊寫 DB 或 Redis 操作
    // DB::table('urls')->update(...);
    $this->comment('同步完成');
})->everyMinute();

註:在 Laravel 11/12 中,你可以直接在指令定義後串接 ->everyMinute(),它會自動幫你註冊進排程。

三種方式的比較

特性Schedule::job()Artisan::command()Schedule::call()
主要用途適合耗時長、需放入隊列的任務適合需要手動執行 & 排程的任務適合執行任意一段 PHP 程式碼
開發便利性高(邏輯解耦)最高(可手動 php artisan ... 測試)中(邏輯通常較碎)
可測試性易於單獨測試 Job易於終端機測試指令較難獨立測試

建議

如果你希望平常也能手動輸入指令來同步數據(例如:php artisan shorturl:sync-clicks),那麼使用 方法一 是最佳選擇。它讓你的開發與維運(Debug)變得非常容易。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *


內容索引