高流量媒體系統的 Laravel 實戰課題:從 Legacy 重構到金流、快取與 AI 開發(進階架構版)
在內容媒體與訂閱平台的開發中,後端團隊通常面對的是「在營運不中斷的前提下,邊換輪胎邊開車」的架構演進課題。
以媒體事業群的數位系統為例,常見的技術痛點包含:新舊系統並存時的 Session 與驗證同步、流量爆發時的快取防線、訂閱金流的絕不重扣與狀態衝突、千萬級巨量資料庫效能瓶頸、多節點零停機部署,以及導入 AI 工具時的程式碼資安防線。
本文拆解這六大核心場景,提供實務上的系統架構設計、程式碼實作與落地解法。
一、 Legacy Code 重構:新舊系統共存與 SSO / Session 一致性
1. 舊系統痛點與架構解耦
舊版 CodeIgniter (CI3) 系統常見的問題在於 Fat Controller、原生 SQL 散落與同步 Blocking 第三方呼叫。重構的第一步是透過 Service / Repository Pattern 將業務邏輯與資料存取抽離,並以抽象層封裝外部 HTTP 呼叫。
2. CI 與 Laravel 雙系統共存的 SSO / Session 挑戰
在逐步重構(Strangler Fig Pattern)的過渡期,使用者會在舊 CI 頁面與新 Laravel 頁面之間無縫切換。此時最大的挑戰是:如何維持單一登入(SSO)與 Session 狀態一致性?
┌────────────────────────────────────────────────────────┐
│ Client Browser │
└───────────┬────────────────────────────────┬───────────┘
│ Request (Cookie: app_session) │ Request (Cookie: app_session)
▼ ▼
┌───────────────────────┐ ┌───────────────────────┐
│ CodeIgniter 3 (舊系統) │ │ Laravel 11 (新系統) │
└───────────┬───────────┘ └───────────┬───────────┘
│ │
│ ┌────────────────────────┐ │
└──►│ Central Redis Session │◄──┘
│ Store (JSON / Custom) │
└────────────────────────┘
實務解法:中央化 Redis Session 與客製化 Handler
- 共享 Cookie 網域:將 Session Cookie 設定在頂層網域(如
.yourdomain.com),確保兩邊都能讀取相同的 Cookie Key。 - Session 序列化格式統一:CI3 預設使用 PHP 原生
serialize,而 Laravel 預設使用自己的 Encrypter 與 Serializer。解法是建立統一的 Redis Session Handler:- 方案 A(推薦):在 Laravel 端建立客製化的 Session Driver,讓它解讀 CI3 儲存於 Redis 中的 JSON 格式 Session 資料。
- 方案 B(JWT / Central SSO):引入輕量級 JWT 簽章機制。當使用者在舊系統登入後,核發包含
user_id的 Signed Cookie,Laravel 與 CI3 分別建立 Middleware 驗證該 Token。
PHP
// Laravel 端的 Custom Session Handler 示意
namespace App\Extensions;
use Illuminate\Support\Facades\Redis;
class SharedCiSessionHandler implements \SessionHandlerInterface
{
public function read($sessionId): string|false
{
// 讀取 CI3 寫入 Redis 的 Session 格式
$data = Redis::connection('session')->get("ci_session:" . $sessionId);
if (!$data) return '';
$unserialized = unserialize($data);
// 轉換為 Laravel 可識別的 Session 結構
return json_encode([
'auth_user_id' => $unserialized['user_id'] ?? null,
'user_role' => $unserialized['role'] ?? 'guest',
]);
}
// ... 實作其餘 SessionHandler 介面方法
}
🧠 思考題 1.1當使用者在舊 CI 系統點擊「登出」時,如何確保 Laravel 系統的 Session 即時失效,且不會產生競態條件(Race Condition)?
二、 高流量文章頁的多層快取與 Edge 防禦
當重大新聞爆發時,資料庫最大的敵人不是常態流量,而是 Cache Miss 瞬間的驚群效應(Cache Stampede)。完整防線應延伸至邊緣節點(CDN Edge)。
[ User Request ]
│
▼
┌─────────────────────────────────┐
│ CDN Edge (Cloudflare / Fastly) ├─► Hit: 回傳 Stale / Cached Page (0ms)
│ Cache-Control + SWR Header │
└────────────────┬────────────────┘
│ Miss / Stale Revalidate
▼
┌─────────────────────────────────┐
│ Redis Cluster (L2 Cache) ├─► Hit: 回傳 JSON Data
│ (帶 Redis Lock 防驚群機制) │
└────────────────┬────────────────┘
│ Miss (搶到 Lock 者)
▼
┌─────────────────────────────────┐
│ MySQL Database │
└─────────────────────────────────┘
1. CDN Edge 與 Stale-While-Revalidate (SWR) 模式
在文章 Response Header 中注入
Cache-Control 指令,啟用 SWR 機制:HTTP
Cache-Control: public, max-age=60, stale-while-revalidate=300, stale-if-error=86400
max-age=60:60 秒內直接由 CDN 邊緣快取回應。stale-while-revalidate=300:60 秒~360 秒之間,CDN 會先回傳舊頁面給使用者,並在背景非同步發送一個 Request 回 Laravel 重新整理快取。stale-if-error=86400:若源站(Laravel/MySQL)崩潰,CDN 可繼續提供長達 24 小時的過期快取頁面。
2. L2 Redis 快取與 Lock 防禦實作
PHP
public function getArticleDetail(int $articleId): array
{
$cacheKey = "article:v1:detail:{$articleId}";
// 1. 讀取快取
$data = Cache::get($cacheKey);
if ($data) return $data;
// 2. 使用 Atomic Lock 防禦 Cache Stampede
$lock = Cache::lock("lock:{$cacheKey}", 5);
if ($lock->get()) {
try {
$data = Article::with(['category', 'tags'])->findOrFail($articleId)->toArray();
Cache::put($cacheKey, $data, now()->addMinutes(10));
} finally {
$lock->release();
}
return $data;
}
// 3. 未搶到鎖者:微幅等待後讀取,或走 Fallback
usleep(100000); // 100ms
return Cache::get($cacheKey) ?? $this->getFallbackArticleData($articleId);
}
🧠 思考題 2.1如果 Redis Cluster 發生整體故障(Outage),系統應該如何實施優雅降級(Graceful Degradation),避免全站請求直接刷爆 MySQL?
三、 訂閱制與金流 Webhook 的可靠性與狀態衝突處理
訂閱制的核心原則為:絕對不重複開通、不遺漏權限、能處理非同步狀態不一致。
[ 金流 Webhook ]
│
▼
[ Signature 驗證 ] ──► (失敗則 400 Abort)
│
▼
[ DB Unique Index 鎖定 ] ──► (重複則 200 OK 跳過)
│
▼
[ 狀態機 State Machine 檢查 ]
│
├─► (Webhook 狀態 == API 查詢狀態) ──► 執行權限開通 DB Transaction
│
└─► (狀態衝突 / 延遲) ───────────────► 觸發二次查詢與衝突事件監聽
1. 處理 Webhook 延遲與 API 查詢不一致的實務案例
在真實場景中,金流平台重送的 Webhook 狀態(如
Pending 或 Paid)可能與我們主動透過 API 查詢的結果(如 Failed 或 Expired)發生衝突。解法:有限狀態機(Finite State Machine, FSM)與雙向校驗
- 嚴格控制訂單狀態流轉:訂單狀態只允許單向演進:
Pending$\rightarrow$Paid/Failed$\rightarrow$Refunded。不允許已進入Paid的訂單受延遲到的PendingWebhook 覆蓋。 - 強制主動主動主查 (Active Query Verification):當收到 Webhook 通知扣款成功時,不完全信任 Payload,後端主動發起一次 TLS 對向 API 查詢金流端,雙重驗證為
Success後才發放權限。
PHP
public function handleSpGatewayWebhook(Request $request)
{
if (!$this->verifySignature($request)) {
return response()->json(['message' => 'Invalid signature'], 400);
}
$tradeNo = $request->input('TradeNo');
$orderId = $request->input('MerchantOrderNo');
return DB::transaction(function () use ($tradeNo, $orderId, $request) {
// 1. 利用 DB 排他鎖 (Pessimistic Lock) 鎖定訂單
$order = Order::where('id', $orderId)->lockForUpdate()->firstOrFail();
// 2. 狀態機防禦:已完成的訂單直接回傳,防止重複處理
if ($order->status === OrderStatus::PAID) {
return response()->json(['message' => 'Already processed'], 200);
}
// 3. 雙重校驗:向金流 API 發起 Secondary Check
$apiResult = $this->paymentGatewayClient->queryOrder($tradeNo);
if ($apiResult->isPaid() && $request->input('Status') === 'SUCCESS') {
$order->update(['status' => OrderStatus::PAID, 'paid_at' => now()]);
UserSubscription::grantAccess($order->user_id, $order->plan_id);
return response()->json(['message' => 'Success'], 200);
}
// 情況:狀態衝突 (Webhook 成功但 API 查無結果或失敗)
Log::critical("Payment mismatch for Order {$orderId}", ['webhook' => $request->all(), 'api' => $apiResult]);
$order->update(['status' => OrderStatus::FLAGGED_FOR_REVIEW]);
return response()->json(['message' => 'State mismatch recorded'], 200);
});
}
🧠 思考題 3.1如果金流平台系統異常,延遲了 30 分鐘才回傳交易結果,而使用者在前端頁面不斷重整並提示「付款處理中」,後端應如何設計權限預發放與過期補償機制?
四、 千萬級閱讀紀錄的大表優化與替代架構
當
user_article_views 資料表達到數千萬層級時,在 OLTP 資料庫(MySQL)執行 GROUP BY 與 ORDER BY 會嚴重消耗 CPU 資源。1. 替代方案選型分析
| 方案 | 適用情境 | 優點 | 缺點 / 成本 |
| Redis ZSET | 即時熱門排行榜 (前 100 名) | 極速 ($O(\log N)$)、實時更新 | 記憶體成本高,資料持久化需謹慎 |
| ClickHouse | 巨量行為 Log 分析、多維度統計 | 欄位式儲存,百億級資料毫秒響應 | 需維護獨立 OLAP 叢集,不支援高頻單筆更新 |
| TimescaleDB | 時間序列分析 (按時間區段統計) | 相容 PostgreSQL,具自動分區 (Hypertables) | 需由 MySQL 轉移至 PostgreSQL 生態系 |
2. Redis ZSET 與 ClickHouse 雙軌架構
[ User Article View Event ]
│
├──► (即時) ──► Redis ZSET (ZINCRBY leaderboard:24h 1 article_id)
│
└──► (異步/Log) ─► Vector / Kafka ──► ClickHouse (OLAP 巨量分析)
ClickHouse 數據導流設計
對於千萬級點擊日誌,利用 Laravel Event 非同步發送至 Kafka/S3,再批次匯入 ClickHouse 做多維度統計(例如:按地區、裝置、作者統計 30 天熱門度)。
🧠 思考題 4.1當採用 ClickHouse 作為分析資料庫時,為了避免每筆閱讀都直接寫入 ClickHouse 導致小檔案過多 (Too many parts),Laravel 應如何設計雙發 (Dual Write) 或 Buffer 批次寫入機制?
五、 CI/CD 零停機部署與多節點同步
在多節點(Multi-Node / ECS Cluster)架構下,滾動更新(Rolling Update)期間會同時存在舊版本程式碼與新版本程式碼的 Container。
[ Load Balancer ]
│
┌──────────────────┴──────────────────┐
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ Node A (Old Code) │ │ Node B (New Code) │
└───────────┬──────────┘ └───────────┬──────────┘
│ │
└──────────────────┬──────────────────┘
▼
┌──────────────────────┐
│ Shared MySQL DB │
└──────────────────────┘
1. 解決多節點競態 Migration
若 5 個 Node 同時啟動並執行
php artisan migrate,會造成 DB Table Lock 競爭甚至部署失敗。- 解法:使用 Laravel 11 的
migrate --isolated旗標(底層使用 Atomic Lock 機制),確保整個 Cluster 同一時間只有一個 Node 能執行 Migration。
Bash
php artisan migrate --force --isolated
2. 多節點 Cache 不一致防禦
部署時若執行
php artisan config:cache 或 cache:clear,可能導致 Node A 刷除了 Redis 快取,但 Node B 仍在使用舊版序列化類別,造成 UnserializeException。- 版本化快取 Key (Versioned Cache Prefix):在
config/app.php或.env中定義APP_VERSION(例如 Git Commit Hash)。 - 快取 Key 統一帶上版本字串:
article:v_{APP_VERSION}:detail:{id}。部署升級後,新舊版本的 Code 各自讀寫獨立版本號的 Cache,升級完成後再讓舊 Cache 自然過期。
🧠 思考題 5.1在藍綠部署(Blue-Green Deployment)過程中,若新的 Migration 包含刪除舊欄位(DROP COLUMN),應該在部署的哪一個階段執行?為什麼?
六、 AI 開發工作流與 CI/CD 安全防線
團隊導入 AI(如 Cursor, GitHub Copilot)能大幅提升開發效率,但 AI 生成的程式碼常帶有 N+1 SQL 查詢、Mass Assignment 風險 或 弱型別漏洞。
1. 安全自動化檢查工具鏈 (Security Pipeline)
必須在 GitHub Actions CI Pipeline 中設置自動化靜態分析閘門,攔截不合規的 AI 程式碼:
YAML
# .github/workflows/ai-code-quality.yml 範例片段
name: AI Code Guardrails
on: [push, pull_request]
jobs:
static-analysis:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
tools: phpstan, composer-normalize
# 1. 靜態分析:設定嚴格層級 (Level 8) 捕捉型別與潛在 Bug
- name: Run PHPStan / Larastan
run: vendor/bin/phpstan analyse --level=8
# 2. 資安掃描:檢查第三方套件 CVE 漏洞
- name: Security Vulnerability Audit
run: composer audit
# 3. 程式碼風格與架構規則約束
- name: Check Code Architecture
run: vendor/bin/deptrac analyse
2. 定義 .cursorrules 限制 AI 產出
在專案根目錄設定
.cursorrules,強制要求 AI 遵循團隊的最佳實踐:Plaintext
- 所有的 DB 查詢必須使用 Eloquent 或 Query Builder,嚴禁產生 RAW SQL 拼接。
- 在產生 Controller 時,必須搭配 FormRequest 處理驗證,嚴禁在 Controller 內撰寫 $request->validate()。
- 查詢包含關聯模型時,必須預先加載 (Eager Loading with()),嚴禁在 foreach 內觸發 N+1 查詢。
- 所有敏感欄位寫入必須經過 $fillable 白名單保護。
🧠 思考題 6.1如何在 CI 流程中整合 Larastan 與 Static Analysis 工具,自動偵測 AI 程式碼中未被with()預載入的潛在 N+1 查詢?
結語
維運與設計高流量媒體系統,關鍵在於深諳系統的防禦邊界與降級機制。
從 Legacy 重構的 Session 共存,到邊緣快取的 SWR 模式、金流狀態機的雙向校驗、巨量資料的 OLAP 轉型,以及 CI/CD 與 AI 工作流的自動化防線——將複雜的業務情境拆解成具備「可觀測」、「可恢復」與「高容錯」的架構步驟,才是系統長期穩健演進的真正基石。
沒有留言:
張貼留言