Django Caching 快取策略完全指南 | Django 教學

2026/06/21 2026/05/26
Django Caching 快取策略完全指南 | Django 教學

當你的 Django 應用程式開始面對大量流量時,快取(Caching) 是提升效能最直接有效的手段。透過將頻繁存取的資料暫存在高速儲存層,可以大幅減少 資料庫查詢 次數並加速回應速度。本文將從快取基礎概念出發,帶你認識 Django 支援的各種 快取後端(Cache Backend)RedisMemcachedLocMemCache,接著深入 View-Level 快取Template Fragment 快取Low-Level 快取 API 三種不同粒度的快取機制,最後探討 快取失效策略版本管理 的最佳實踐。

快取基礎概念

在 Web 應用程式中,每次使用者發出請求時,伺服器通常需要執行資料庫查詢、模板渲染、商業邏輯運算等一系列操作。當某些資料或頁面在短時間內不會變動時,重複執行這些操作就是一種浪費。快取(Caching) 的核心概念就是「把計算結果暫存起來,下次直接取用」。

Django 提供了一套多層快取架構,從粗粒度到細粒度,讓你根據不同需求選擇合適的快取策略:

快取層級說明適用場景
Per-site Cache(全站快取)透過 CacheMiddleware 快取整個網站內容幾乎不變的靜態網站
Per-view Cache(單頁快取)使用 @cache_page 快取單一 View 的回應整頁內容不常變動
Template Fragment(模板片段快取)使用 {% cache %} 標籤快取模板中的特定區塊頁面中某個耗時元件
Low-level API(程式碼級快取)使用 cache.set() / cache.get() 精細控制需要精確管理快取邏輯

選擇原則:整頁幾乎不變時使用 Per-view Cache;頁面中某個區塊特別耗時時使用 Template Fragment;需要在程式碼中精細控制快取邏輯,或跨多個 View 共用計算結果時,使用 Low-level API。


Django 快取後端

Django 支援多種快取後端,每種後端各有優缺點,適合不同的使用場景。

Redis(生產環境首選)

Redis 是目前生產環境中最推薦的快取後端,透過 django-redis 套件與 Django 整合。Redis 不僅速度極快,還支援持久化、豐富的資料結構(List、Set、Hash 等)和 Pub/Sub 機制。

# 安裝 django-redis
pip install django-redis

Memcached

Memcached 是老牌的分散式快取系統,適合純快取場景。它的特性是極速、簡單的 Key-Value 儲存,但不支援持久化,伺服器重啟後資料全部遺失。

DatabaseCache(資料庫快取)

使用資料庫表格作為快取儲存,不需要額外安裝服務,但每次快取操作仍然需要查詢資料庫,效能增益有限。適合開發環境或簡單的部署場景。

# 建立快取資料表
python manage.py createcachetable

FileBasedCache(檔案快取)

將快取資料以檔案形式儲存在磁碟上,設定簡單但 I/O 效能較差,適合開發環境使用。

LocMemCache(本地記憶體快取)

Django 的預設快取後端,將資料儲存在應用程式的記憶體中。優點是速度快、不需要額外服務;缺點是程序重啟即失效,且不同程序之間無法共享快取。僅適合單程序的開發環境。

以下是各後端的比較:

後端適用場景優點缺點
Redis生產環境首選持久化、豐富資料結構、Pub/Sub需安裝 Redis 服務
Memcached純快取場景極速、分散式支援無持久化、僅 KV 儲存
Database開發/簡單環境不需額外服務額外查詢 DB,效能有限
File開發環境設定簡單I/O 效能差
LocMemCache單程序開發零設定、速度快不跨程序共享、重啟失效

CACHES 設定

Django 透過 settings.py 中的 CACHES 字典來配置快取後端。以下示範最常用的 Redis 後端設定:

# settings.py

CACHES = {
    'default': {
        'BACKEND': 'django_redis.cache.RedisCache',
        'LOCATION': 'redis://127.0.0.1:6379/1',
        'OPTIONS': {
            'CLIENT_CLASS': 'django_redis.client.DefaultClient',
            'COMPRESSOR': 'django_redis.compressors.zlib.ZlibCompressor',  # 壓縮大型資料
            'IGNORE_EXCEPTIONS': True,  # Redis 失效時不拋出例外(降級處理)
        },
        'KEY_PREFIX': 'myproject',   # 所有 key 的前綴,避免多專案衝突
        'TIMEOUT': 300,              # 預設過期時間(秒),None = 永不過期
        'VERSION': 1,                # 版本號,用於批次失效
    },
}

你也可以設定多個快取後端,針對不同用途使用不同的 Redis 資料庫:

# settings.py — 多快取後端設定

CACHES = {
    'default': {
        'BACKEND': 'django_redis.cache.RedisCache',
        'LOCATION': 'redis://127.0.0.1:6379/1',
        'OPTIONS': {'CLIENT_CLASS': 'django_redis.client.DefaultClient'},
        'KEY_PREFIX': 'myproject',
        'TIMEOUT': 300,
    },
    'sessions': {
        'BACKEND': 'django_redis.cache.RedisCache',
        'LOCATION': 'redis://127.0.0.1:6379/2',
        'OPTIONS': {'CLIENT_CLASS': 'django_redis.client.DefaultClient'},
    },
}

其他後端的設定範例:

# Memcached 後端
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache',
        'LOCATION': '127.0.0.1:11211',
    }
}

# 資料庫後端
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.db.DatabaseCache',
        'LOCATION': 'my_cache_table',  # 資料表名稱
    }
}

# 檔案後端
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
        'LOCATION': '/var/tmp/django_cache',  # 快取檔案目錄
    }
}

# 本地記憶體後端(預設)
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
        'LOCATION': 'unique-snowflake',  # 用於區分不同的記憶體快取實例
    }
}

View-Level 快取(@cache_page)

@cache_page 裝飾器(Decorator)是最簡單的快取方式,它會將整個 View 的 HTTP 回應快取指定的秒數。在快取有效期間內,Django 不會再執行 View 函式,而是直接回傳快取的回應。

函式導向視圖(FBV)

from django.views.decorators.cache import cache_page

# 快取整頁回應 15 分鐘(900 秒)
@cache_page(60 * 15)
def article_list(request):
    articles = Article.objects.all()
    return render(request, 'articles/list.html', {'articles': articles})

類別導向視圖(CBV)

from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
from django.views.generic import ListView

@method_decorator(cache_page(60 * 15), name='dispatch')
class ArticleListView(ListView):
    model = Article

在 URL 設定中套用

如果不想修改 View 的程式碼,也可以直接在 URL 設定中套用 cache_page

from django.views.decorators.cache import cache_page

urlpatterns = [
    path('articles/', cache_page(60 * 15)(ArticleListView.as_view()), name='article_list'),
]

依條件分別快取(Vary Headers)

當同一個 URL 需要根據不同條件回傳不同內容時(例如多語言頁面、已登入/未登入使用者),需要搭配 vary_on_headersvary_on_cookie 讓 Django 分別快取:

from django.views.decorators.vary import vary_on_headers, vary_on_cookie
from django.views.decorators.cache import cache_page, cache_control

# 依 Accept-Language header 分別快取(多語言頁面)
@vary_on_headers('Accept-Language')
@cache_page(3600)
def multilingual_view(request):
    pass

# 依 Cookie 分別快取(已登入/未登入使用者)
@vary_on_cookie
@cache_page(600)
def conditional_view(request):
    pass

# 設定 Cache-Control headers(控制瀏覽器與 CDN 快取行為)
@cache_control(max_age=3600, public=True)   # CDN 可快取
def public_api_view(request):
    pass

@cache_control(private=True, max_age=300)   # 只允許瀏覽器快取
def user_profile_view(request):
    pass

Template Fragment 快取

當整頁快取粒度太粗,但你又不想深入程式碼控制時,Template Fragment 快取(模板片段快取) 是一個很好的中間方案。它使用 {% cache %} 模板標籤,只快取模板中的特定區塊。

首先在模板中載入 cache 標籤庫:

{% load cache %}

{# 快取整個導覽列 600 秒 #}
{% cache 600 navbar %}
    <nav>
        {% for item in nav_items %}
            <a href="{{ item.url }}">{{ item.name }}</a>
        {% endfor %}
    </nav>
{% endcache %}

{% cache %} 標籤的語法為 {% cache 過期秒數 快取名稱 [可選的變動參數...] %}。當你需要依據不同條件分別快取時,可以傳入額外的參數:

{# 依使用者分別快取(不同使用者不共用快取) #}
{% cache 300 user_sidebar request.user.pk %}
    <div class="sidebar">
        <p>您好,{{ request.user.username }}</p>
        {% for notification in user.notifications.all %}
            <p>{{ notification.message }}</p>
        {% endfor %}
    </div>
{% endcache %}

{# 依語言分別快取 #}
{% cache 3600 homepage request.LANGUAGE_CODE %}
    {# 多語言首頁內容 #}
{% endcache %}

Template Fragment 快取的優勢在於:不需要修改 View 程式碼,直接在模板中決定哪些區塊需要快取,非常適合頁面中只有某個區塊特別耗時的情境。


Low-Level 快取 API

Low-Level 快取 API(程式碼級快取) 提供最細緻的控制能力,讓你在 Python 程式碼中直接操作快取。Django 透過 django.core.cache 模組提供統一的 API 介面。

基本操作:set / get / delete

from django.core.cache import cache

# 設定快取(300 秒後過期)
cache.set('my_key', 'my_value', timeout=300)

# 設定永不過期的快取
cache.set('permanent_key', 'value', timeout=None)

# 取得快取(不存在時回傳 None)
value = cache.get('my_key')

# 取得快取,指定預設值
value = cache.get('my_key', default='fallback')

# 刪除快取
cache.delete('my_key')

# 清空整個快取(謹慎使用!)
cache.clear()

原子操作:get_or_set

get_or_set() 是非常實用的原子操作,如果 key 不存在,就執行計算並設定快取,避免了 競態條件(Race Condition)

from django.core.cache import cache

# 快取不存在時才執行 lambda 計算,並將結果存入快取
value = cache.get_or_set('expensive_key', lambda: compute_expensive(), timeout=600)

批次操作

當需要同時讀寫多個 key 時,批次操作可以減少網路往返次數,提升效能:

# 批次設定
cache.set_many({'key1': 'v1', 'key2': 'v2'}, timeout=300)

# 批次取得(不存在的 key 不會出現在結果中)
values = cache.get_many(['key1', 'key2', 'key3'])
# 結果:{'key1': 'v1', 'key2': 'v2'}

# 批次刪除
cache.delete_many(['key1', 'key2'])

計數器(原子遞增/遞減)

cache.set('visit_count', 0)
cache.incr('visit_count')       # 原子遞增 1
cache.incr('visit_count', 10)   # 原子遞增 10
cache.decr('visit_count')       # 原子遞減 1

使用特定快取後端

如果在 CACHES 中設定了多個快取後端,可以透過 caches 物件指定使用哪一個:

from django.core.cache import caches

# 使用名為 'sessions' 的快取後端
session_cache = caches['sessions']
session_cache.set('token', 'abc123', timeout=86400)

實際應用:快取資料庫查詢

以下是一個常見的 Cache-aside(旁路快取) 模式範例,先查快取、快取未命中時再查資料庫:

from django.core.cache import cache

def get_featured_articles():
    """快取精選文章列表"""
    cache_key = 'featured_articles'
    articles = cache.get(cache_key)

    if articles is None:
        # 快取 Miss:從資料庫查詢
        articles = list(
            Article.objects.filter(is_featured=True)
            .select_related('author')
            .order_by('-created_at')[:10]
        )
        cache.set(cache_key, articles, timeout=3600)  # 快取 1 小時

    return articles

快取失效策略

快取最困難的部分不是「如何設定快取」,而是「何時讓快取失效」。過期的快取會導致使用者看到舊資料(Stale Data),但太頻繁地清除快取又會失去快取的意義。

基於時間的失效(TTL)

最簡單的策略,設定快取的過期時間(TTL, Time To Live),到期後自動失效:

cache.set('article_list', articles, timeout=300)  # 5 分鐘後自動失效

主動失效(Signal-based Invalidation)

當資料更新時,透過 Django Signal(訊號) 主動清除相關快取,確保使用者看到最新資料:

from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from django.core.cache import cache

@receiver(post_save, sender=Article)
@receiver(post_delete, sender=Article)
def invalidate_article_caches(sender, instance, **kwargs):
    """任何 Article 變更時,使相關快取失效"""
    keys_to_delete = [
        f'articles:detail:{instance.pk}:v1',
        'featured_articles',
        'article_count',
    ]
    cache.delete_many(keys_to_delete)

避免 Cache Stampede(雷群效應)

Cache Stampede(雷群效應) 是指高流量下快取 key 同時過期時,大量請求同時衝擊資料庫造成瞬間過載的問題。

解決方案一:分散式鎖(Mutex Lock)

import time
from django.core.cache import cache

def get_article_count_safe():
    cache_key = 'article_count'
    lock_key = f'{cache_key}:lock'

    count = cache.get(cache_key)
    if count is not None:
        return count

    # 嘗試取得鎖(cache.add 是原子操作,只有一個請求能成功)
    acquired = cache.add(lock_key, 1, timeout=10)

    if acquired:
        try:
            count = Article.objects.count()
            cache.set(cache_key, count, timeout=300)
        finally:
            cache.delete(lock_key)
    else:
        # 其他請求短暫等待後取得快取值
        time.sleep(0.1)
        count = cache.get(cache_key, default=0)

    return count

解決方案二:Early Expiration(提前更新)

在 TTL 到期前就提前更新快取,避免多個請求同時發現快取過期:

import time
from django.core.cache import cache

def get_with_early_expiration(key, compute_fn, ttl=300, early_factor=0.9):
    """在 TTL 剩餘 10% 時就開始提前更新"""
    data = cache.get(f'{key}:ext')

    if data is None:
        # 完全 miss,重新計算
        value = compute_fn()
        expire_at = time.time() + ttl
        cache.set(f'{key}:ext', {'value': value, 'expire_at': expire_at}, timeout=ttl)
        return value

    value = data['value']
    # 進入 early expiration 視窗時,允許一個請求提前更新
    if time.time() > data['expire_at'] * early_factor:
        new_value = compute_fn()
        expire_at = time.time() + ttl
        cache.set(f'{key}:ext', {'value': new_value, 'expire_at': expire_at}, timeout=ttl)
        return new_value

    return value

快取鍵版本管理

隨著應用程式的演進,快取中儲存的資料結構可能會改變。如果舊版快取與新版程式碼不相容,就會導致錯誤。Django 內建了 版本化快取(Cache Versioning) 機制來解決這個問題。

全域版本號

CACHES 設定中指定 VERSION,部署新版本時只需遞增版本號,所有舊快取就會自動失效:

# settings.py
CACHES = {
    'default': {
        'BACKEND': 'django_redis.cache.RedisCache',
        'LOCATION': 'redis://127.0.0.1:6379/1',
        'VERSION': 2,  # 部署時遞增,所有快取自動失效
    }
}

個別 Key 的版本控制

你也可以在操作個別 key 時指定版本號:

# 設定與取得特定版本的快取
cache.set('user_profile', data, version=2)
cache.get('user_profile', version=2)

# 遞增版本號(不影響其他版本的 key)
cache.incr_version('user_profile')

Cache Key 命名最佳實踐

良好的 Cache Key 命名規則可以讓快取管理更加清晰:

class CacheKeyBuilder:
    """快取 Key 建構器:統一命名規則"""
    VERSION = 'v1'

    @staticmethod
    def article_detail(article_id: int) -> str:
        # 格式:<namespace>:<entity>:<identifier>:<version>
        return f'articles:detail:{article_id}:{CacheKeyBuilder.VERSION}'

    @staticmethod
    def article_list(page: int, category: str = 'all') -> str:
        return f'articles:list:{category}:page{page}:{CacheKeyBuilder.VERSION}'

    @staticmethod
    def user_profile(user_id: int) -> str:
        return f'users:profile:{user_id}:{CacheKeyBuilder.VERSION}'

使用 django-redis 的 Pattern 刪除(批次失效)

當你需要一次清除某個前綴下的所有快取時,可以透過 django-redis 的原生 Redis 連線執行 pattern 刪除:

from django_redis import get_redis_connection

def invalidate_article_list_cache():
    """使所有文章列表快取失效"""
    redis = get_redis_connection('default')
    # 使用 scan_iter 避免 KEYS 命令阻塞 Redis
    pattern = 'myproject:articles:list:*'
    for key in redis.scan_iter(pattern):
        redis.delete(key)

注意:在生產環境中,應避免使用 redis.keys() 命令,因為它會掃描整個資料庫並阻塞 Redis。建議改用 scan_iter() 進行非阻塞的迭代刪除。


Cache Warming(快取預熱)

部署新版本或 Redis 重啟後,快取會完全清空,大量請求同時衝擊資料庫。Cache Warming(快取預熱) 策略是在服務啟動前,主動將熱門資料載入快取:

# management/commands/warm_cache.py
from django.core.management.base import BaseCommand
from django.core.cache import cache
from articles.models import Article

class Command(BaseCommand):
    help = '預熱快取:將熱門資料預先載入 Redis'

    def handle(self, *args, **options):
        self.stdout.write('開始預熱快取...')

        # 預熱精選文章
        featured = list(
            Article.objects.filter(is_featured=True)
            .select_related('author')[:20]
        )
        cache.set('featured_articles', featured, timeout=3600)
        self.stdout.write(f'  精選文章:{len(featured)} 筆')

        # 預熱熱門分類
        from django.db.models import Count
        categories = list(
            Article.objects.values('category')
            .annotate(count=Count('id'))
            .order_by('-count')[:10]
        )
        cache.set('top_categories', categories, timeout=86400)
        self.stdout.write(f'  熱門分類:{len(categories)} 個')

        self.stdout.write(self.style.SUCCESS('快取預熱完成!'))
# 部署後執行快取預熱
python manage.py warm_cache

總結

本文全面介紹了 Django 的快取機制,從基礎概念到進階策略:

  1. 快取後端選擇:生產環境首選 Redis,開發環境可使用 LocMemCache 或 DatabaseCache,不同後端各有適用場景
  2. CACHES 設定:透過 settings.py 配置快取後端,支援多後端同時使用,KEY_PREFIX 避免多專案衝突
  3. View-Level 快取@cache_page 快取整頁回應,搭配 vary_on_headers 依條件分別快取
  4. Template Fragment 快取{% cache %} 標籤快取模板片段,不需修改 View 程式碼
  5. Low-Level APIcache.set() / cache.get() / cache.delete() 提供最精細的程式碼級控制,get_or_set() 避免競態條件
  6. 快取失效策略:TTL 自動過期搭配 Signal 主動失效,使用分散式鎖或 Early Expiration 避免 Cache Stampede
  7. 版本管理:全域 VERSION 設定搭配 Cache Key 命名規則,確保快取與程式碼版本一致

快取是一把雙面刃:用得好能大幅提升效能,用不好則會導致資料不一致。關鍵在於根據業務場景選擇合適的快取粒度與失效策略。下一篇我們將介紹 Django + Celery 非同步任務佇列,學習如何將耗時操作從 HTTP 請求中分離出來,進一步提升應用程式的回應速度。

BenZ Software Developer

熱愛技術的軟體開發者,在這裡分享程式開發經驗與學習筆記。