Redis 應用場景:快取、佇列與速率限制
你已經建立了一個網頁應用程式,在少數使用者下運作良好,但隨著流量成長,你注意到頁面載入變慢、背景工作重複執行,以及 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;
}
快取最佳實踐
- 設定 TTL:始終設定過期時間,以避免資料過時和記憶體膨脹。
- 使用適當的資料結構:對於簡單的鍵值,使用字串。對於物件,使用雜湊以單獨更新欄位。
- 處理快取失效:當底層資料變更時,更新或刪除快取項目。
- 監控命中率:低命中率表示你的快取無效;調整鍵或 TTL。
快取失效策略
失效很難。常見策略包括:
- 基於時間:依賴 TTL。簡單但可能提供過時資料。
- 寫入穿透:每當更新資料庫時更新快取。
- 寫入後置:先更新快取,然後非同步更新資料庫(有資料遺失風險)。
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