laravel 對於資料庫連線的檢查

在文章Laravel 將「點閱記錄」改成非同步 queue 寫入資料庫中,有時會發生Redis服務沒啟動,導致系統發生錯誤,針對這個問題,可以建立一個 Laravel Trait,用來檢查資料庫與 Redis 是否有正常連線,並可封裝為共用的「健康檢查套件」。這在系統監控、API 心跳檢查(health check)、DevOps 整合中很實用。

讓它能檢查以下項目:

項目驗證內容
Database資料庫是否可連線
RedisRedis 是否能 set/get 成功
QueueLaravel queue driver 是否正常
CacheLaravel cache 是否能寫入與讀取

步驟一:建立 Trait

<?php

namespace App\Traits;

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Cache;
use Illuminate\Queue\RedisQueue;
use App\Jobs\TestQueueJob;
use Throwable;

trait SystemHealthCheck
{
    /**
     * 檢查資料庫連線
     * @return bool
     */
    public function checkDatabaseConnection(): bool
    {
        try {
            DB::connection()->getPdo();
            return true;
        } catch (Throwable $e) {
            report($e);
            return false;
        }
    }

    /**
     * 檢查 Redis 連線
     * @return bool
     */
    public function checkRedisConnection(): bool
    {
        try {
            Redis::set('health_check', 'ok');
            return Redis::get('health_check') === 'ok';
        } catch (Throwable $e) {
            report($e);
            return false;
        }
    }

    /**
     * 檢查 Queue 是否可用
     * @return bool
     */
    public function checkQueue(): bool
    {
        try {
            $connection = Queue::connection();

            // 若為 Redis queue 才呼叫 ping()
            if ($connection instanceof RedisQueue) {
                $ping = $connection->getRedis()->ping();
                return $ping === '+PONG';
            }

            // 其他 driver fallback:派送同步 job 測試
            TestQueueJob::dispatchSync();
            return true;
        } catch (\Throwable $e) {
            report($e);
            return false;
        }
    }

    /**
     * 檢查暫存 Cache 是否可用
     * @return bool
     */
    public function checkCache(): bool
    {
        try {
            Cache::put('health_check_cache', 'ok', now()->addSeconds(5));
            return Cache::get('health_check_cache') === 'ok';
        } catch (Throwable $e) {
            report($e);
            return false;
        }
    }

    /**
     * 回報系統狀態
     * @return array{cache: string, database: string, queue: string, redis: string}
     */
    public function systemStatus(): array
    {
        return [
            'database' => $this->checkDatabaseConnection() ? 'ok' : 'fail',
            'redis'    => $this->checkRedisConnection() ? 'ok' : 'fail',
            'queue'    => $this->checkQueue() ? 'ok' : 'fail',
            'cache'    => $this->checkCache() ? 'ok' : 'fail',
        ];
    }
}

建立 Job: TestQueueJob

<?php

namespace App\Jobs;

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Foundation\Queue\Queueable;

class TestQueueJob implements ShouldQueue
{
    use Queueable, Dispatchable;

    /**
     * Execute the job.
     */
    public function handle()
    {
        return true;
    }
}

步驟二:建立使用該 Trait 的 Controller

// app/Http/Controllers/SystemCheckController.php

namespace App\Http\Controllers;

use Illuminate\Http\JsonResponse;
use App\Traits\SystemHealthCheck;

class SystemCheckController extends Controller
{
    use SystemHealthCheck;

    public function status(): JsonResponse
    {
        $status = $this->systemStatus();
        $isHealthy = collect($status)->every(fn ($v) => $v === 'ok');

        return response()->json([
            'status' => $isHealthy ? 'ok' : 'fail',
            'services' => $status
        ], $isHealthy ? 200 : 500);
    }
}

步驟三:設定路由

// routes/web.php 或 routes/api.php

use App\Http\Controllers\SystemCheckController;

Route::get('/health-check', [SystemCheckController::class, 'status']);

Middleware: LogBlogPostView 程式修正

<?php

namespace App\Http\Middleware;

use App\Jobs\LogBlogPostViewJob;
use App\Traits\SystemHealthCheck;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use App\Models\Blog\BlogPost;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Queue;

class LogBlogPostView
{
    use SystemHealthCheck;
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next): Response
    {
        $postId = $request->route('id') ?? $request->route('post'); // 支援不同路由命名

        $user = Auth::guard('sanctum')->user();
        $user_id = $user?->id;

        if ($postId) {
            $blogPost = BlogPost::findOrFail($postId);

            if ($blogPost) {
                $ip = $request->ip();
                $host = gethostbyaddr($ip) ?? 'unknown';
                $userAgent = $request->header('User-Agent') ?? 'unknown';
                $today = Carbon::now()->toDateString();

                $cacheKey = "blog_viewed:{$postId}:{$ip}:{$today}:{$user_id}";

                // 檢查是否使用 Redis 及可用性
                if (env('QUEUE_CONNECTION') == 'redis' && !$this->checkRedisConnection()) {
                    throw new \Exception("Redis Service is not available", 404);
                }
                // 先檢查 Cache 若有值,改為非同步任務派送
                if (!Cache::has($cacheKey)) {
                    // 放入table:cache裡
                    Cache::put($cacheKey, true, now()->endOfDay());

                    // 放入table:jobs裡, 下面兩行指令是相同, 擇一即可
                    LogBlogPostViewJob::dispatch($postId, $ip, $host, $userAgent, $user_id);
                    //Queue::push(new LogBlogPostViewJob($postId, $ip, $host, $userAgent, $user_id));

                    // 更新文章總點閱數
                    $blogPost->increment('view_count');
                }
            }
        }

        return $next($request);
    }
}

問題分析

錯誤訊息:

Call to undefined method Illuminate\Queue\DatabaseQueue::getConnection()

表示這行出錯:

$connection = Queue::connection();
$connection->getRedis()->ping(); // ❌ DatabaseQueue 沒這個方法

getRedis() 只在 RedisQueue 類別中存在,不適用於 database、sync、sqs 等其他 driver。

正確解法:根據 driver 類型做條件判斷

我們應該偵測目前的 queue driver,只有在使用 redis 時才呼叫 ping(),否則用 fallback 方法(例如 dispatch 測試 job)。

✔ 修正後 checkQueue() 實作:

use Illuminate\Support\Facades\Queue;
use Illuminate\Queue\RedisQueue;
use App\Jobs\TestQueueJob;

public function checkQueue(): bool
{
    try {
        $connection = Queue::connection();

        // 若為 Redis queue 才呼叫 ping()
        if ($connection instanceof RedisQueue) {
            $ping = $connection->getRedis()->ping();
            return $ping === '+PONG';
        }

        // 其他 driver fallback:派送同步 job 測試
        TestQueueJob::dispatchSync();
        return true;
    } catch (\Throwable $e) {
        report($e);
        return false;
    }
}

發佈留言

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


內容索引