結論先講

大多數情況,直接用 Redis。 它能當快取、Session Store、訊息佇列、排行榜、分散式鎖——幾乎是後端的瑞士刀。Memcached 在 2026 年只剩下一個優勢:多線程在純快取場景的記憶體效率稍高。至於 Session 管理——小專案 DB 就夠,中大型專案 Redis Session,JWT 則要看你願不願意接受它的缺點。

快取不難,快取失效才難。 這篇也會講那個讓人崩潰的話題。


為什麼需要快取?

先看數字:

操作延遲
Redis GET~0.1-0.5 ms
PostgreSQL 簡單查詢(有索引)~1-5 ms
PostgreSQL 複雜 JOIN~10-100 ms
外部 API 呼叫~50-500 ms
沒有索引的全表掃描~100-5000 ms

快取的目的很簡單:把頻繁讀取的結果放在更快的地方。 記憶體比磁碟快 100 倍,這就是為什麼 Redis 和 Memcached 都把資料放在記憶體裡。

但快取不是萬能藥。加了快取就多了一層要維護的東西:快取和資料庫的一致性、快取穿透、快取雪崩。不要在不需要的時候加快取。

什麼時候不需要快取?

  • 你的 DB 查詢已經夠快(< 5ms)
  • 你的流量不大(< 100 QPS)
  • 你的資料變動很頻繁,快取存活時間很短
  • 你是 Serverless 架構(冷啟動可能比快取查詢還慢)

Redis vs Memcached

功能RedisMemcached
資料結構String, List, Set, Hash, Sorted Set, Stream, HyperLogLog只有 String
持久化RDB 快照 + AOF 日誌無(純記憶體)
Pub/Sub支援不支援
Lua 腳本支援(原子性操作)不支援
叢集Redis Cluster(自動分片)Client-side 一致性雜湊
線程模型單線程事件迴圈(6.0+ I/O 多線程)多線程
記憶體效率有 overhead(資料結構元資料)較高(Slab allocator)
最大 value512 MB1 MB(預設)
過期策略精確到毫秒精確到秒
社群活躍度極高(Valkey fork 也很活躍)

結論

2026 年選 Redis(或其開源 fork Valkey)。Memcached 的唯一優勢是「純快取場景的記憶體效率」,但你省的那點記憶體,換來的是失去 Pub/Sub、Sorted Set、持久化等所有進階功能。


Session 管理方案比較

方案優點缺點適合
DB Session簡單、不用多裝東西、可以查詢每次請求都查 DB、需要清理過期 Session小型專案、低流量
Redis Session快、自動過期、支援叢集多一個服務要維護中大型專案
JWT無狀態、不查 DB、跨服務容易無法即時撤銷、Token 可能很大、安全考量多API 服務、微服務
Cookie Session (Signed)不需後端存儲大小受限(4KB)、每次請求都帶極簡單的應用

DB Session

最直覺的做法。Express 用 connect-pg-simple,Django 內建就有:

# Django settings.py — 用資料庫存 Session(預設行為)
SESSION_ENGINE = 'django.contrib.sessions.backends.db'
SESSION_COOKIE_AGE = 86400 * 7  # 7 天
 
# 記得定期清理
# python manage.py clearsessions

Redis Session

Express + Redis 的經典組合:

import express from 'express';
import session from 'express-session';
import RedisStore from 'connect-redis';
import { createClient } from 'redis';
 
const redisClient = createClient({ url: 'redis://localhost:6379' });
await redisClient.connect();
 
const app = express();
 
app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: true,      // HTTPS only
    httpOnly: true,     // 防 XSS
    maxAge: 86400000,   // 1 天(毫秒)
    sameSite: 'lax'     // 防 CSRF
  }
}));
 
app.get('/profile', (req, res) => {
  if (!req.session.userId) return res.status(401).json({ error: 'Not logged in' });
  res.json({ userId: req.session.userId });
});

Django + Redis:

# settings.py
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
SESSION_CACHE_ALIAS = 'default'
 
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.redis.RedisCache',
        'LOCATION': 'redis://localhost:6379/1',
    }
}

JWT 的真相

JWT 不是 Session 的「升級版」,它是不同的架構選擇。

// JWT 的好處:無狀態,任何服務都能驗證
import jwt from 'jsonwebtoken';
 
// 簽發
const token = jwt.sign(
  { userId: 123, role: 'admin' },
  process.env.JWT_SECRET,
  { expiresIn: '1h' }
);
 
// 驗證——不用查資料庫
const decoded = jwt.verify(token, process.env.JWT_SECRET);

JWT 的致命缺點:無法即時撤銷。

用戶改密碼、被停權、登出——你發出去的 JWT 在過期前都是有效的。解決方案(黑名單、短效 token + refresh token)都很複雜,而且一旦你用了黑名單,你就是在用有狀態的 JWT——那為什麼不直接用 Session?

我的建議: 單體應用用 Redis Session。微服務之間的 service-to-service 認證可以用 JWT。不要因為「JWT 比較潮」就用它來做用戶登入。


快取策略

Cache-Aside(旁路快取)

最常見的模式。應用自己管理快取。

def get_user(user_id):
    # 1. 先查快取
    cached = redis.get(f"user:{user_id}")
    if cached:
        return json.loads(cached)
 
    # 2. 快取沒有,查資料庫
    user = db.query("SELECT * FROM users WHERE id = %s", user_id)
 
    # 3. 寫入快取
    redis.setex(f"user:{user_id}", 3600, json.dumps(user))
 
    return user
 
def update_user(user_id, data):
    # 1. 更新資料庫
    db.execute("UPDATE users SET name = %s WHERE id = %s", data['name'], user_id)
 
    # 2. 刪除快取(不是更新!)
    redis.delete(f"user:{user_id}")

關鍵:更新時刪除快取,不是更新快取。 下次讀取時會自動從 DB 載入最新資料。這樣可以避免「先更新快取還是先更新 DB」的競爭條件。

Write-Through(寫穿透)

寫入同時更新 DB 和快取。保證快取永遠是最新的,但寫入延遲增加。

Write-Behind(寫回)

先寫快取,定期批次寫入 DB。寫入速度快,但有資料丟失風險。

策略讀取速度寫入速度一致性複雜度
Cache-Aside快(命中時)正常最終一致
Write-Through較慢強一致
Write-Behind弱一致

大部分情況用 Cache-Aside 就對了。


快取失效:電腦科學的兩大難題

“There are only two hard things in Computer Science: cache invalidation and naming things.” — Phil Karlton

常見問題

快取穿透(Cache Penetration): 查詢不存在的資料,每次都打到 DB。

# 解法:快取空結果
def get_user(user_id):
    cached = redis.get(f"user:{user_id}")
    if cached == "NULL":     # 空結果也快取
        return None
    if cached:
        return json.loads(cached)
 
    user = db.query("SELECT * FROM users WHERE id = %s", user_id)
    if user is None:
        redis.setex(f"user:{user_id}", 300, "NULL")  # 快取空結果 5 分鐘
        return None
 
    redis.setex(f"user:{user_id}", 3600, json.dumps(user))
    return user

快取雪崩(Cache Avalanche): 大量快取同時過期,DB 瞬間被打爆。

import random
 
# 解法:加隨機過期時間
base_ttl = 3600  # 1 小時
jitter = random.randint(0, 600)  # 隨機 0-10 分鐘
redis.setex(key, base_ttl + jitter, value)

快取擊穿(Cache Breakdown): 熱門 key 過期的瞬間,大量請求同時打 DB。

# 解法:分散式鎖,只讓一個請求去 DB
def get_hot_data(key):
    cached = redis.get(key)
    if cached:
        return json.loads(cached)
 
    lock_key = f"lock:{key}"
    if redis.set(lock_key, "1", nx=True, ex=10):  # 拿到鎖
        data = db.query(...)
        redis.setex(key, 3600, json.dumps(data))
        redis.delete(lock_key)
        return data
    else:
        time.sleep(0.1)  # 等別人更新
        return get_hot_data(key)  # 重試

Redis 實戰模式

Redis 不只是快取。以下是幾個常用的設計模式。

Rate Limiting(API 限流)

def is_rate_limited(user_id, limit=100, window=60):
    """滑動視窗限流:每分鐘最多 100 次"""
    key = f"rate:{user_id}"
    now = time.time()
 
    pipe = redis.pipeline()
    pipe.zremrangebyscore(key, 0, now - window)  # 移除過期記錄
    pipe.zadd(key, {str(now): now})              # 加入當前請求
    pipe.zcard(key)                               # 計算視窗內請求數
    pipe.expire(key, window)                      # 設定 key 過期
    _, _, count, _ = pipe.execute()
 
    return count > limit

Leaderboard(排行榜)

# Sorted Set 天生適合排行榜
redis.zadd("game:leaderboard", {"player_a": 1500, "player_b": 2300, "player_c": 1800})
 
# 前 10 名(分數由高到低)
top10 = redis.zrevrange("game:leaderboard", 0, 9, withscores=True)
# [('player_b', 2300.0), ('player_c', 1800.0), ('player_a', 1500.0)]
 
# 我的排名
rank = redis.zrevrank("game:leaderboard", "player_a")  # 0-based

Distributed Lock(分散式鎖)

import uuid
 
def acquire_lock(lock_name, timeout=10):
    """取得分散式鎖"""
    identifier = str(uuid.uuid4())
    # NX: 只在 key 不存在時設定(原子操作)
    if redis.set(f"lock:{lock_name}", identifier, nx=True, ex=timeout):
        return identifier
    return None
 
def release_lock(lock_name, identifier):
    """釋放鎖(Lua 腳本確保原子性)"""
    script = """
    if redis.call('get', KEYS[1]) == ARGV[1] then
        return redis.call('del', KEYS[1])
    else
        return 0
    end
    """
    redis.eval(script, 1, f"lock:{lock_name}", identifier)

用 Lua 腳本釋放鎖是為了防止「我的鎖過期後被別人拿到,我卻把別人的鎖刪了」。


什麼時候不需要 Redis?

場景為什麼不用
小型應用(< 100 QPS)DB 查詢已經夠快,加快取反而增加複雜度
ServerlessLambda 冷啟動建立 Redis 連線的時間可能比查 DB 還久
資料極少如果整個資料庫只有幾千筆,DB 本身就是「在記憶體裡」(OS page cache)
強一致性需求快取和 DB 之間必然有短暫不一致的視窗
預算極低多一台 Redis 就是多一份主機費和維運工

常見問題

Redis 資料掉了怎麼辦?

開啟 AOF 持久化(appendonly yes),每次寫入都會記錄到磁碟。但即使開了 AOF,也不該把 Redis 當唯一的資料來源——它是快取層,主資料永遠在 DB。

Redis 用多少記憶體才夠?

經驗法則:你的熱資料大小 x 1.5(Redis 的資料結構 overhead)。可以用 redis-cli INFO memory 查看即時用量。另外一定要設 maxmemory 和淘汰策略(allkeys-lru 最常用)。

JWT 和 Redis Session 可以一起用嗎?

可以。常見做法是 Access Token 用短效 JWT(15 分鐘),Refresh Token 存在 Redis。這樣兼顧無狀態(大部分請求不查 DB)和可撤銷(刪 Redis 裡的 Refresh Token 就好)。

Memcached 還有人用嗎?

有,Facebook(Meta)就是 Memcached 的大戶。但那是因為他們在 Memcached 上建了整套自研基礎設施。新專案沒理由選 Memcached。

Redis 7.0 有什麼重要更新?

Redis Functions(取代 EVAL Lua)、Sharded Pub/Sub、ACL v2。另外 2024 年的授權爭議產生了 Valkey(Linux Foundation 的 Redis fork),如果你在意開源授權,可以考慮 Valkey。


本系列文章

  1. 資料庫全景圖:一張表看懂所有類型
  2. MySQL:為什麼越來越多人覺得它不夠好?
  3. PostgreSQL:為什麼它變成了預設選擇
  4. 快取與 Session 管理:Redis、Memcached、還是直接用 DB?(本篇)
  5. 全文搜尋:Elasticsearch、Meilisearch、還是 PostgreSQL 就夠了?
  6. NoSQL 什麼時候該用
  7. 資料庫設計:正規化與索引策略
  8. SQL 效能調校
  9. 資料庫遷移實戰
  10. 擴展策略:讀寫分離與分片
  11. Docker 環境的資料庫管理
  12. 資料庫監控與告警