iT邦幫忙

2025 iThome 鐵人賽

DAY 21
0

昨天我們完成了後台的訂單狀態修改。接下來要讓系統在真實場景更「順」:後台列表更快、通知更即時、尖峰時段也不爆。

今天我們導入 Redis,示範三個實用情境:
(1)快取 Dashboard 訂單列表
(2)以清除策略維持資料一致
(3)用清單/佇列處理通知工作

最後補一個 Docker 快速起跑 段落,讓你 10 分鐘內把 Redis 跑起來。


為什麼是 Redis?

  • 超快的 in‑memory 資料存取:減少重複查詢 MongoDB 的成本。

  • 好用的資料結構:String/Hash/List/Set/Sorted Set,能做快取、排行榜、佇列等。

  • Pub/Sub & Stream:方便做即時通知與背景工作。


Docker 安裝與啟動提醒

⚠️ 環境需求提醒

若你尚未安裝 Docker,請先安裝:

  • Windows / macOS:安裝 Docker Desktop
  • Linux:可使用原生 Docker Engine

安裝後請確認 Docker 服務有啟動:

docker info

若出現錯誤訊息如:

open //./pipe/dockerDesktopLinuxEngine: The system cannot find the file specified

代表 Docker Desktop 尚未啟動,請打開軟體並等待鯨魚圖示變成「Running」狀態。

確認方式:執行 docker ps 應能看到目前運行的容器清單。


快速起跑(Docker)

# docker-compose.yml
services:
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    command: ["redis-server", "--appendonly", "yes"]

啟動:

docker compose up -d

https://ithelp.ithome.com.tw/upload/images/20251005/20178868hRr2XoBbiI.png

啟動之後就可以去 docker desktop 中確認 redis 是否有打起來。
https://ithelp.ithome.com.tw/upload/images/20251005/20178868VYmt113qrC.png

Node.js 套件(選其一):

# node-redis (官方) v4
npm i redis
# 或 ioredis(進階用法多、連線管理更強)
npm i ioredis

連線初始化(node-redis v4)

// 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();

情境 1:快取後台訂單列表(TTL)

目標: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 秒常見。對「即時性」要求很高的頁面可設更短或改用主動失效,讓快取保持新鮮、不爆記憶體。


情境 2:資料一致性——主動清除(Cache Invalidation)

目標:當訂單狀態更新時,清掉相關快取,避免舊資料殘留。

// 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),讓多台服務同步清快取。


情境 3:用 Redis 當「簡單佇列」發送通知

目標:狀態改成 Completed 時,把「通知顧客」這件事丟給背景工作(非同步),避免卡住 API 回應。

3.1 產生工作(Producer)

// 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 });
}

3.2 消費工作(Worker)

// 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 負責「慢慢做」。

架構圖
https://ithelp.ithome.com.tw/upload/images/20251005/20178868v7fj6Yz5dS.png

Redis 扮演的角色:

元件 功能
Producer(生產者) 把任務放進 Redis list(例如 queue:notify
Worker(消費者) 不斷監聽 Redis,取出任務來處理
Redis list 任務暫存區(先進先出 FIFO)

Redis 提供 rPush(放入尾端)和 lPop/blPop(從頭取出)來實現佇列。


進階:即時畫面更新(Pub/Sub,選讀)

若想在 Dashboard 即時看到狀態變化,可在後端於狀態更新後 PUBLISH 一個事件;前端以 WebSocket 或 SSE 收聽(本篇點到即可,最後一周有餘力的話,我們再來做)。


安全與營運注意

  • TTL 與清除策略:列表用 TTL + 主動刪;明細頁可不快取或短 TTL。

  • 連線池與逾時:設定 socket: { reconnectStrategy }(ioredis 有更彈性策略)。

  • 觀測:為快取命中率、佇列堆積量加上 metrics(之後 Day 22 的 Logging 會接)。


Redis GUI 工具推薦

雖然 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 的快取與佇列實作。

連線方式:

  1. 打開 RedisInsight → 點「Add Redis Database」。

  2. 填入 Host: localhostPort: 6379

  3. 點「Connect」即可查看所有 key、TTL 倒數與內容。

https://ithelp.ithome.com.tw/upload/images/20251005/20178868CGknyuxZ1i.png


重點回顧

  • 引入 Redis 後,讀快取 + 主動失效 可讓後台列表明顯加速。

  • 以 List + Worker 的方式,就能做出「簡單佇列」,把推播、寄信等工作丟到背景執行。

下一步(Day 22),我們會補上 錯誤處理與 Logging,讓快取與佇列在實務環境更可靠。


上一篇
訂單狀態修改:後台操作訂單流程
下一篇
讓錯誤看得見:Logging 與錯誤處理設計
系列文
用 LINE OA 打造中小企業訂單系統:從零開始的 30 天實作紀錄22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言