昨天我們完成了後台的訂單狀態修改。接下來要讓系統在真實場景更「順」:後台列表更快、通知更即時、尖峰時段也不爆。
今天我們導入 Redis,示範三個實用情境:
(1)快取 Dashboard 訂單列表
(2)以清除策略維持資料一致
(3)用清單/佇列處理通知工作
最後補一個 Docker 快速起跑 段落,讓你 10 分鐘內把 Redis 跑起來。
超快的 in‑memory 資料存取:減少重複查詢 MongoDB 的成本。
好用的資料結構:String/Hash/List/Set/Sorted Set,能做快取、排行榜、佇列等。
Pub/Sub & Stream:方便做即時通知與背景工作。
⚠️ 環境需求提醒
若你尚未安裝 Docker,請先安裝:
安裝後請確認 Docker 服務有啟動:
docker info
若出現錯誤訊息如:
open //./pipe/dockerDesktopLinuxEngine: The system cannot find the file specified
代表 Docker Desktop 尚未啟動,請打開軟體並等待鯨魚圖示變成「Running」狀態。
✅ 確認方式:執行 docker ps
應能看到目前運行的容器清單。
# docker-compose.yml
services:
redis:
image: redis:7-alpine
ports:
- "6379:6379"
command: ["redis-server", "--appendonly", "yes"]
啟動:
docker compose up -d
啟動之後就可以去 docker desktop 中確認 redis 是否有打起來。
Node.js 套件(選其一):
# node-redis (官方) v4
npm i redis
# 或 ioredis(進階用法多、連線管理更強)
npm i ioredis
// src/lib/redis.js
const { createClient } = require("redis");
const redis = createClient({
url: process.env.REDIS_URL || "redis://localhost:6379",
});
redis.on("error", (err) => console.error("Redis error", err));
async function ensureConnected() {
if (!redis.isOpen) await redis.connect();
return redis;
}
module.exports = { redis, ensureConnected };
在 src/index.js
入口最早初始化一次(或於用到時 lazy connect):
require("dotenv").config();
const { ensureConnected } = require("./lib/redis");
ensureConnected();
目標:Dashboard 每次進來都查整批訂單很慢;用 Redis 快取 30 秒,顯著縮短 TTFB。
// src/routes/order.js(擷取)
const express = require("express");
const router = express.Router();
const { authAdmin } = require("./auth");
const Order = require("../models/order.model");
const { ensureConnected } = require("../lib/redis");
router.get("/", authAdmin, async (req, res) => {
const redis = await ensureConnected();
const KEY = "orders:list:v1"; // 版本號協助日後兼容
// 1) 先讀快取
const cached = await redis.get(KEY);
if (cached) {
return res.json({ success: true, source: "cache", orders: JSON.parse(cached) });
}
// 2) 查 DB,寫入快取(設定 TTL 30 秒)
const orders = await Order.find().sort({ createdAt: -1 }).lean();
await redis.set(KEY, JSON.stringify(orders), { EX: 30 });
res.json({ success: true, source: "db", orders });
});
何時用 TTL(Time To Live, 存活時間)?列表類、變動頻率中等的資料;30–60 秒常見。對「即時性」要求很高的頁面可設更短或改用主動失效,讓快取保持新鮮、不爆記憶體。
目標:當訂單狀態更新時,清掉相關快取,避免舊資料殘留。
// src/routes/order.js(擷取)
router.patch("/:id/status", authAdmin, async (req, res) => {
const { status } = req.body;
if (!["Pending", "In Progress", "Completed"].includes(status)) {
return res.status(400).json({ message: "不合法的狀態" });
}
const order = await Order.findByIdAndUpdate(req.params.id, { status }, { new: true });
if (!order) return res.status(404).json({ message: "找不到訂單" });
// 1) 清除列表快取
const { ensureConnected } = require("../lib/redis");
const redis = await ensureConnected();
await redis.del("orders:list:v1");
res.json({ success: true, order });
});
規模再大時,可在更新處理完成後發出事件(例如用 Redis Pub/Sub),讓多台服務同步清快取。
目標:狀態改成 Completed
時,把「通知顧客」這件事丟給背景工作(非同步),避免卡住 API 回應。
// src/queues/notify.js
const { ensureConnected } = require("../lib/redis");
const QUEUE_KEY = "queue:notify:v1";
async function enqueueNotify(payload) {
const redis = await ensureConnected();
// 用 RPUSH 把工作放到尾端
await redis.rPush(QUEUE_KEY, JSON.stringify(payload));
}
module.exports = { enqueueNotify, QUEUE_KEY };
在狀態更新 API 內(完成時)丟工作:
if (status === "Completed") {
const { enqueueNotify } = require("../queues/notify");
enqueueNotify({ type: "order_completed", orderId: order._id });
}
// worker/notify-worker.js
require("dotenv").config();
const { ensureConnected } = require("../src/lib/redis");
const { QUEUE_KEY } = require("../src/queues/notify");
(async () => {
const redis = await ensureConnected();
console.log("Notify worker started.");
while (true) {
// BLPOP:阻塞式取出最前面的工作(timeout 5s)
const res = await redis.blPop(QUEUE_KEY, 5);
if (!res) continue; // 超時重試
const [, job] = res; // [queue, value]
try {
const payload = JSON.parse(job);
if (payload.type === "order_completed") {
// TODO: 呼叫 LINE Messaging API 推播通知顧客
console.log("send notify for order", payload.orderId);
}
} catch (err) {
console.error("Notify job error", err);
// 失敗可選擇:記錄到 dead-letter、或重試入隊
}
}
})();
優點:API 回應不被通知延遲拖慢;Worker 可橫向擴充。需要更完整的重試/排程機制時,可考慮 BullMQ、Bee-Queue。
為什麼要 Producer / Worker?
我們目前的 API 是:
管理員按下「訂單完成」 → 後端更新 DB → 回傳成功
但現實中還可能要通知顧客(LINE 推播)、發信、更新報表、紀錄歷史 Log
這些都不是「當下就該阻塞 API」的事。
於是我們就讓:
API 只負責「丟任務」,
Worker 負責「慢慢做」。
架構圖
Redis 扮演的角色:
元件 | 功能 |
---|---|
Producer(生產者) | 把任務放進 Redis list(例如 queue:notify ) |
Worker(消費者) | 不斷監聽 Redis,取出任務來處理 |
Redis list | 任務暫存區(先進先出 FIFO) |
Redis 提供
rPush
(放入尾端)和lPop
/blPop
(從頭取出)來實現佇列。
若想在 Dashboard 即時看到狀態變化,可在後端於狀態更新後 PUBLISH
一個事件;前端以 WebSocket 或 SSE 收聽(本篇點到即可,最後一周有餘力的話,我們再來做)。
TTL 與清除策略:列表用 TTL + 主動刪;明細頁可不快取或短 TTL。
連線池與逾時:設定 socket: { reconnectStrategy }
(ioredis 有更彈性策略)。
觀測:為快取命中率、佇列堆積量加上 metrics(之後 Day 22 的 Logging 會接)。
雖然 Redis 是命令列導向的資料庫,但也有許多像 MongoDB Compass 一樣的圖形化工具可以使用:
工具名稱 | 開發者 | 特點 | 適合對象 |
---|---|---|---|
RedisInsight | Redis 官方 | 免費、跨平台、功能完整、支援 JSON/Hash/List,內建 CLI 與統計圖表 | ✅ 最推薦,完整度最高 |
Another Redis Desktop Manager (ARDM) | 開源社群 | 輕量快速、支援 SSH、多資料庫 | 想快速瀏覽 key 的前端 / 後端工程師 |
Redis Commander | 社群維護 | Web UI,透過 npm 安裝 (npm install -g redis-commander ),可遠端管理 |
想要 Web 版 Redis 管理介面的使用者 |
💡 建議使用 RedisInsight:可視化 key、TTL、queue 狀態,特別適合用來觀察 Day 21 的快取與佇列實作。
連線方式:
打開 RedisInsight → 點「Add Redis Database」。
填入 Host: localhost
、Port: 6379
。
點「Connect」即可查看所有 key、TTL 倒數與內容。
引入 Redis 後,讀快取 + 主動失效 可讓後台列表明顯加速。
以 List + Worker 的方式,就能做出「簡單佇列」,把推播、寄信等工作丟到背景執行。
下一步(Day 22),我們會補上 錯誤處理與 Logging,讓快取與佇列在實務環境更可靠。