Redis 應用場景:快取、佇列與速率限制

Backend2026-10-06TryQuickToolBox

你已經建立了一個網頁應用程式,在少數使用者下運作良好,但隨著流量成長,你注意到頁面載入變慢、背景工作重複執行,以及 API 被濫用客戶端猛打。這些是經典的擴展問題,而 Redis 可以優雅地解決。Redis 是一個記憶體內資料儲存,擅長三項核心任務:快取、佇列和速率限制。在本文中,我們將透過實用範例和最佳實踐來探討每個應用場景。

為什麼在這些場景中使用 Redis?

Redis 將資料儲存在記憶體中,使其速度極快——通常回應時間低於毫秒。它支援豐富的資料結構,如字串、列表、集合、有序集合和雜湊,這些自然對應到快取、佇列和速率限制。它還提供原子操作、發布/訂閱和持久化選項,使其成為後端開發人員的多功能工具。

1. 使用 Redis 進行快取

快取是最常見的 Redis 應用場景。透過將頻繁存取的資料儲存在 Redis 中,你可以減少資料庫負載並加速回應。

快取如何運作

當請求進來時,檢查資料是否存在於 Redis 中。如果存在(快取命中),直接返回。如果不存在(快取未命中),從資料庫獲取,將其儲存在 Redis 中並設定過期時間,然後返回。這種模式稱為 cache-aside。

// Node.js 範例,使用 ioredis
const Redis = require('ioredis');
const redis = new Redis();

async function getUser(userId) {
  const cacheKey = `user:${userId}`;
  const cached = await redis.get(cacheKey);
  if (cached) return JSON.parse(cached);

  const user = await db.query('SELECT * FROM users WHERE id = ?', [userId]);
  await redis.set(cacheKey, JSON.stringify(user), 'EX', 3600); // 1 小時 TTL
  return user;
}

快取最佳實踐

快取失效策略

失效很難。常見策略包括:

2. 使用 Redis 的佇列

佇列將耗時任務與請求-回應週期解耦。例如,發送電子郵件、處理圖片或生成報告可以非同步完成。

實作簡單佇列

Redis 列表非常適合佇列。使用 LPUSH 將工作新增到左側,使用 BRPOP 阻塞並從右側彈出(FIFO)。

// 生產者:新增工作
await redis.lpush('email_queue', JSON.stringify({ to: 'user@example.com', subject: 'Welcome' }));

// 消費者:處理工作(在工作執行緒中)
while (true) {
  const job = await redis.brpop('email_queue', 0); // 0 = 無限阻塞
  const { to, subject } = JSON.parse(job[1]);
  await sendEmail(to, subject);
}

使用 BRPOPLPUSH 的可靠佇列

為了避免工作執行緒崩潰時遺失工作,使用 BRPOPLPUSH 原子地將工作移到處理中列表。成功處理後,從處理中列表移除它。如果工作執行緒死亡,另一個工作執行緒可以從處理中列表恢復工作。

const job = await redis.brpoplpush('email_queue', 'processing_queue', 0);
try {
  // 處理工作
  await redis.lrem('processing_queue', 1, job);
} catch (err) {
  // 處理錯誤,可能重新排隊
}

專用佇列函式庫

在生產環境中,考慮使用 Bull (Node.js)、RQ (Python) 或 Sidekiq (Ruby) 等函式庫。它們提供重試、排程和監控功能。

3. 使用 Redis 進行速率限制

速率限制保護你的 API 免受濫用並確保公平使用。Redis 的原子操作使其非常適合計數請求。

固定視窗速率限制

為每個使用者在每個時間視窗內遞增計數器。如果計數超過限制,則拒絕請求。

async function isAllowed(userId, limit = 100, windowSec = 60) {
  const key = `rate:${userId}:${Math.floor(Date.now() / 1000 / windowSec)}`;
  const count = await redis.incr(key);
  if (count === 1) await redis.expire(key, windowSec);
  return count