Next.js 高併發架構實戰:如何優雅解決 Load Balance 下的快取同步問題

2026-01-201.47Front EndReactNext.js 高併發架構實戰:如何優雅解決 Load Balance 下的快取同步問題

前言

在當前的 Web 開發中,將 Next.js 容器化並透過 Nginx 做負載平衡(Load Balancing)是常見手段。然而,當我們開啟多個 App 實例(Replicas)來處理高流量時,最令人頭痛的往往不是算力不足,而是「狀態不一致」

如果沒有一個統一的快取層,使用者可能會遇到這種鬼故事:重新整理第一次看到新資料(命中 Container A),再重新整理一次又變回舊資料(命中 Container B)。

本文將詳細拆解我們如何透過 Docker Compose 基礎設施與 Next.js 自定義 Cache Handler,打造一個既能承載高流量,又具備 Redis 強大快取同步能力的架構。

基礎設施層:Docker Compose 編排

想要高可用,首先得讓 App「分身」。我們利用 Docker Compose 的 replicas 功能,輕易啟動 3 個 Next.js 實例,並透過 Nginx 當作前門(Gateway)來分流。

看看我們的 
docker-compose.yml
關鍵配置:

version: "3.8" services: # 1. Nginx:流量入口與負載平衡器 nginx: image: nginx:alpine ports: - "80:80" depends_on: - next-app # 確保 App 起來了再導流 # ... 省略 volume 與 network 設定 # 2. Next.js App:核心應用 next-app: build: . environment: - REDIS_URL=redis://redis-cache:6379 # 指向內網的 Redis Service deploy: replicas: 3 # 關鍵:啟動 3 個容器實例 healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"] interval: 30s retries: 3 # 3. Redis:共享狀態的單一來源 (Single Source of Truth) redis-cache: image: redis/redis-stack-server:latest # 使用 Stack 版本以支援更多功能 ports: - "6379:6379" healthcheck: test: ["CMD", "redis-cli", "ping"]

架構亮點:

Replicas = 3:這裡直接宣告啟動 3 個 Next.js 容器,Nginx 會自動透過 DNS Round-Robin 或配置好的 Upstream 將流量平均分配。
Healthcheck:非常重要。Docker 會定時戳 /api/health,如果有某個 Container 掛掉或卡死,流量就不會導過去,甚至可以設定自動重啟。
Redis Stack:我們選擇 redis-stack-server 而非普通 Redis,是為了保留未來使用 RedisJSON 或 Search 等進階功能的彈性。

應用層配置:接管 Next.js 快取

Next.js 預設是將快取存在本機檔案系統(.next/cache),這在多容器環境下是行不通的。我們需要修改 
next.config.ts
來「劫持」這個快取行為。

// next.config.ts import type { NextConfig } from "next"; const nextConfig: NextConfig = { output: "standalone", // 1. 指定我們自己寫的快取處理器 cacheHandler: require.resolve("./cache-handler.js"), // 2. 徹底禁用記憶體快取 // 這是因為 L1 Memory Cache 也是各個容器獨立的,不關掉的話 // 還是會發生資料不一致的問題 cacheMaxMemorySize: 0,  }; export default nextConfig;

這裡的 cacheMaxMemorySize: 0 是一個魔鬼細節。如果不設為 0,Next.js 還是會優先讀取記憶體中的快取,導致我們辛苦架設的 Redis 在某些情況下被繞過。

核心邏輯:優雅降級的 Cache Handler

接下來是重頭戲 
cache-handler.js
我們使用了 @neshca/cache-handler 套件,它提供了一個標準介面讓我們可以輕鬆切換 Redis 與 LRU(記憶體)模式。

我們的策略是 "Redis First, Local Fallback"(Redis 優先,本地保底)。

// cache-handler.js import { CacheHandler } from "@neshca/cache-handler"; import createRedisHandler from "@neshca/cache-handler/redis-stack"; import createLruHandler from "@neshca/cache-handler/local-lru"; import { createClient } from "redis"; import { PHASE_PRODUCTION_BUILD } from "next/constants.js"; CacheHandler.onCreation(async () => { let client; // 技巧:Build 時期不需要連 Redis // 避免 CI/CD 環境沒有 Redis 導致 Build 失敗 if (PHASE_PRODUCTION_BUILD !== process.env.NEXT_PHASE) { try { client = createClient({ url: process.env.REDIS_URL ?? "redis://localhost:6379", }); // Error Listener 是必須的,防止連線錯誤讓整個 Node Process 崩潰 client.on("error", (e) => console.warn("Redis error", e)); await client.connect(); } catch (error) { console.warn("Redis 連線失敗,將降級至本地快取"); } } let redisHandler = null; if (client?.isReady) { // 成功連線:使用 Redis Handler redisHandler = await createRedisHandler({ client, keyPrefix: "next-cache:", timeoutMs: 1000, // 設定超時,避免 Redis 慢拖垮 Web Server }); } // 永遠準備好 LRU Handler 作為備案 const LRUHandler = createLruHandler(); return { // 優先序:先試 Redis,不行就退守 LRU handlers: [redisHandler, LRUHandler], }; }); export default CacheHandler;

這段程式碼解決了幾個工程實務上的痛點:

Build Time Safety:在 npm run build 時,我們通常不需要(也可能無法)連線到生產環境的 Redis。透過 PHASE_PRODUCTION_BUILD 判斷,我們優雅地跳過了連線步驟。
Graceful Degradation:如果 Redis 服務臨時掛掉,網站不會跟著掛掉(500 Error)。程式會自動發現 client 沒準備好,而退化成使用 LRUHandler。雖然此時快取不再同步,但至少使用者依然能瀏覽網頁。
Timeout Control:設定 timeoutMs: 1000。在高並發下,如果快取層回應太慢,我們寧願視為 Cache Miss 重新抓資料,也不要讓使用者的 Request 卡在那邊乾等。

實測網頁效果

總結

這套架構完美結合了 Docker Compose 的水平擴展能力與 Redis 的高性能儲存。

Docker Compose 讓我們一行指令 docker-compose up 就能拉起包含 Load Balancer、Web App Cluster、Cache Database 的完整微服務拓撲。
Custom Cache Handler 則是在這拓撲中穿針引線的靈魂,確保了無論流量打到哪一個容器,使用者體驗都是一致且快速的。
這就是現代化 Next.js 應用落地時,兼顧效能與穩定性的最佳實踐。

參考

Nextjs Cache Handler Redis