Django Caching 快取策略完全指南 | Django 教學
當你的 Django 應用程式開始面對大量流量時,快取(Caching) 是提升效能最直接有效的手段。透過將頻繁存取的資料暫存在高速儲存層,可以大幅減少 資料庫查詢 次數並加速回應速度。本文將從快取基礎概念出發,帶你認識 Django 支援的各種 快取後端(Cache Backend) 如 Redis、Memcached 與 LocMemCache,接著深入 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_headers 或 vary_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 的快取機制,從基礎概念到進階策略:
- 快取後端選擇:生產環境首選 Redis,開發環境可使用 LocMemCache 或 DatabaseCache,不同後端各有適用場景
- CACHES 設定:透過
settings.py配置快取後端,支援多後端同時使用,KEY_PREFIX 避免多專案衝突 - View-Level 快取:
@cache_page快取整頁回應,搭配vary_on_headers依條件分別快取 - Template Fragment 快取:
{% cache %}標籤快取模板片段,不需修改 View 程式碼 - Low-Level API:
cache.set()/cache.get()/cache.delete()提供最精細的程式碼級控制,get_or_set()避免競態條件 - 快取失效策略:TTL 自動過期搭配 Signal 主動失效,使用分散式鎖或 Early Expiration 避免 Cache Stampede
- 版本管理:全域 VERSION 設定搭配 Cache Key 命名規則,確保快取與程式碼版本一致
快取是一把雙面刃:用得好能大幅提升效能,用不好則會導致資料不一致。關鍵在於根據業務場景選擇合適的快取粒度與失效策略。下一篇我們將介紹 Django + Celery 非同步任務佇列,學習如何將耗時操作從 HTTP 請求中分離出來,進一步提升應用程式的回應速度。