Django 的快取框架

動態網站的一個基本權衡是,它們是動態的。每次使用者請求頁面時,網頁伺服器都會進行各種計算 - 從資料庫查詢到範本渲染再到商業邏輯 - 以建立您的網站訪客看到的頁面。從處理開銷的角度來看,這比您的標準從檔案系統讀取檔案的伺服器配置要昂貴得多。

對於大多數網頁應用程式而言,這種開銷並不是什麼大問題。大多數網頁應用程式不是 washingtonpost.comslashdot.org;它們是流量一般的的中小型網站。但對於中高流量的網站來說,盡可能減少開銷至關重要。

這就是快取發揮作用的地方。

快取某物是儲存昂貴計算的結果,以便下次不必執行計算。以下是一些虛擬碼,說明這如何適用於動態產生的網頁

given a URL, try finding that page in the cache
if the page is in the cache:
    return the cached page
else:
    generate the page
    save the generated page in the cache (for next time)
    return the generated page

Django 配備了強大的快取系統,可讓您儲存動態頁面,因此不必為每個請求都進行計算。為了方便起見,Django 提供了不同層次的快取粒度:您可以快取特定檢視的輸出,您可以僅快取難以產生的部分,或者您可以快取整個網站。

Django 還可以很好地與「下游」快取(例如 Squid 和基於瀏覽器的快取)配合使用。這些是您不直接控制,但可以為其提供提示(透過 HTTP 標頭)關於您網站的哪些部分應該被快取以及如何快取的快取類型。

另請參閱

快取框架設計理念 解釋了框架的一些設計決策。

設定快取

快取系統需要少量的設定。也就是說,您必須告訴它您的快取資料應該存在於何處 - 無論是在資料庫中、檔案系統中還是直接在記憶體中。這是一個重要的決定,會影響您的快取效能;是的,某些快取類型比其他快取類型快。

您的快取偏好設定會放在設定檔中的 CACHES 設定中。以下說明了 CACHES 的所有可用值。

Memcached

Memcached 是一種完全基於記憶體的快取伺服器,最初開發用於處理 LiveJournal.com 的高負載,隨後由 Danga Interactive 開源。Facebook 和 Wikipedia 等網站使用它來減少資料庫存取並顯著提高網站效能。

Memcached 作為守護進程執行,並分配了指定的 RAM 容量。它所做的只是為在快取中新增、檢索和刪除資料提供快速介面。所有資料都直接儲存在記憶體中,因此沒有資料庫或檔案系統使用的開銷。

在安裝 Memcached 本身之後,您需要安裝 Memcached 繫結。有幾個可用的 Python Memcached 繫結;Django 支援的兩個是 pylibmcpymemcache

若要將 Memcached 與 Django 搭配使用

  • BACKEND 設定為 django.core.cache.backends.memcached.PyMemcacheCachedjango.core.cache.backends.memcached.PyLibMCCache(取決於您選擇的 memcached 繫結)

  • LOCATION 設定為 ip:port 值,其中 ip 是 Memcached 守護進程的 IP 位址,而 port 是 Memcached 正在執行的連接埠,或設定為 unix:path 值,其中 path 是 Memcached Unix socket 檔案的路徑。

在此範例中,Memcached 使用 pymemcache 繫結在 localhost (127.0.0.1) 連接埠 11211 上執行

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache",
        "LOCATION": "127.0.0.1:11211",
    }
}

在此範例中,Memcached 可透過使用 pymemcache 繫結的本機 Unix socket 檔案 /tmp/memcached.sock 取得

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache",
        "LOCATION": "unix:/tmp/memcached.sock",
    }
}

Memcached 的一個出色功能是它能夠在多個伺服器上共用快取。這表示您可以在多台機器上執行 Memcached 守護進程,並且該程式會將機器群組視為 單一 快取,而無需在每台機器上複製快取值。若要利用此功能,請在 LOCATION 中包含所有伺服器位址,無論是以分號或逗號分隔的字串,或是以清單形式。

在此範例中,快取會在 IP 位址為 172.19.26.240 和 172.19.26.242,且均在連接埠 11211 上執行的 Memcached 執行個體之間共用

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache",
        "LOCATION": [
            "172.19.26.240:11211",
            "172.19.26.242:11211",
        ],
    }
}

在下列範例中,快取會在 IP 位址為 172.19.26.240(連接埠 11211)、172.19.26.242(連接埠 11212)和 172.19.26.244(連接埠 11213)上執行的 Memcached 執行個體之間共用

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache",
        "LOCATION": [
            "172.19.26.240:11211",
            "172.19.26.242:11212",
            "172.19.26.244:11213",
        ],
    }
}

依預設,PyMemcacheCache 後端設定下列選項(您可以在 OPTIONS 中覆寫它們)

"OPTIONS": {
    "allow_unicode_keys": True,
    "default_noreply": False,
    "serde": pymemcache.serde.pickle_serde,
}

關於 Memcached 的最後一點是,基於記憶體的快取有一個缺點:由於快取的資料儲存在記憶體中,如果您的伺服器崩潰,資料將會遺失。顯然,記憶體並非用於永久資料儲存,因此請不要僅依賴基於記憶體的快取作為您唯一的資料儲存方式。毫無疑問,任何 Django 快取後端都不應被用於永久儲存 - 它們都旨在成為快取解決方案,而不是儲存 - 但我們在這裡指出這一點是因為基於記憶體的快取是特別暫時的。

Redis

Redis 是一個可用於快取的記憶體內資料庫。首先,您需要在本機或遠端機器上執行 Redis 伺服器。

在設定 Redis 伺服器後,您需要安裝 Redis 的 Python 繫結。redis-py 是 Django 原生支援的繫結。也建議安裝 hiredis-py 套件。

若要將 Redis 與 Django 搭配使用作為快取後端

  • BACKEND 設定為 django.core.cache.backends.redis.RedisCache

  • LOCATION 設定為指向您的 Redis 執行個體的 URL,使用適當的配置。如需可用配置的詳細資訊,請參閱 redis-py 文件。

例如,如果 Redis 在 localhost (127.0.0.1) 連接埠 6379 上執行

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.redis.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379",
    }
}

通常,Redis 伺服器受到驗證保護。為了提供使用者名稱和密碼,請將它們與 URL 一起新增至 LOCATION

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.redis.RedisCache",
        "LOCATION": "redis://username:password@127.0.0.1:6379",
    }
}

如果您在複製模式中設定了多個 Redis 伺服器,您可以將伺服器指定為以分號或逗號分隔的字串,或是以清單形式。當使用多個伺服器時,寫入操作會在第一個伺服器(主節點)上執行。讀取操作會在隨機選擇的其他伺服器(複本節點)上執行

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.redis.RedisCache",
        "LOCATION": [
            "redis://127.0.0.1:6379",  # leader
            "redis://127.0.0.1:6378",  # read-replica 1
            "redis://127.0.0.1:6377",  # read-replica 2
        ],
    }
}

資料庫快取

Django 可以將其快取資料儲存在您的資料庫中。如果您擁有快速、索引良好的資料庫伺服器,效果最佳。

若要使用資料庫表作為您的快取後端

  • BACKEND 設定為 django.core.cache.backends.db.DatabaseCache

  • LOCATION 設定為 tablename,即資料庫表的名稱。此名稱可以是您想要的任何名稱,只要它是尚未在您的資料庫中使用的有效表名稱即可。

在此範例中,快取表的名稱為 my_cache_table

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.db.DatabaseCache",
        "LOCATION": "my_cache_table",
    }
}

與其他快取後端不同,資料庫快取不支援在資料庫層級自動刪除過期的條目。相反地,過期的快取條目會在每次呼叫 add()set()touch() 時刪除。

建立快取表格

在使用資料庫快取之前,您必須使用此命令建立快取表格

python manage.py createcachetable

這會在您的資料庫中建立一個具有 Django 的資料庫快取系統所期望的正確格式的表格。表格的名稱取自 LOCATION

如果您使用多個資料庫快取,createcachetable 會為每個快取建立一個表格。

如果您使用多個資料庫,createcachetable 會觀察您的資料庫路由器的 allow_migrate() 方法(請參閱下文)。

migrate 相同,createcachetable 不會觸及現有的表格。它只會建立遺失的表格。

若要列印將會執行的 SQL,而不是執行它,請使用 createcachetable --dry-run 選項。

多個資料庫

如果您使用具有多個資料庫的資料庫快取,您還需要為您的資料庫快取表格設定路由指示。為了路由的目的,資料庫快取表格會以名為 CacheEntry 的模型,在名為 django_cache 的應用程式中出現。此模型不會出現在模型快取中,但模型詳細資訊可用於路由目的。

例如,以下路由器會將所有快取讀取操作導向 cache_replica,並將所有寫入操作導向 cache_primary。快取表格只會在 cache_primary 上同步。

class CacheRouter:
    """A router to control all database cache operations"""

    def db_for_read(self, model, **hints):
        "All cache read operations go to the replica"
        if model._meta.app_label == "django_cache":
            return "cache_replica"
        return None

    def db_for_write(self, model, **hints):
        "All cache write operations go to primary"
        if model._meta.app_label == "django_cache":
            return "cache_primary"
        return None

    def allow_migrate(self, db, app_label, model_name=None, **hints):
        "Only install the cache model on primary"
        if app_label == "django_cache":
            return db == "cache_primary"
        return None

如果您未指定資料庫快取模型的路由方向,快取後端會使用 default 資料庫。

如果您不使用資料庫快取後端,您就不需要擔心為資料庫快取模型提供路由指示。

檔案系統快取

基於檔案的後端會將每個快取值序列化並儲存為單獨的檔案。若要使用此後端,請將 BACKEND 設定為 "django.core.cache.backends.filebased.FileBasedCache",並將 LOCATION 設定為合適的目錄。例如,若要將快取資料儲存在 /var/tmp/django_cache 中,請使用此設定

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
        "LOCATION": "/var/tmp/django_cache",
    }
}

如果您使用的是 Windows,請將磁碟機代號放在路徑的開頭,如下所示

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
        "LOCATION": "c:/foo/bar",
    }
}

目錄路徑應為絕對路徑,也就是說,它應該從檔案系統的根目錄開始。您是否在設定結尾加上斜線並不重要。

請確保此設定指向的目錄存在且可讀寫,或是可以由您的網頁伺服器執行的系統使用者建立。繼續上面的範例,如果您的伺服器以使用者 apache 的身分執行,請確保目錄 /var/tmp/django_cache 存在且可由使用者 apache 讀寫,或是可以由使用者 apache 建立。

警告

當快取 LOCATION 包含在 MEDIA_ROOTSTATIC_ROOTSTATICFILES_FINDERS 中時,可能會洩露敏感資料。

存取快取檔案的攻擊者不僅可以偽造您的網站將會信任的 HTML 內容,還可以遠端執行任意程式碼,因為資料是使用 pickle 序列化的。

警告

當儲存大量檔案時,檔案系統快取可能會變慢。如果您遇到此問題,請考慮使用不同的快取機制。您也可以對 FileBasedCache 進行子類別化,並改進刪除策略。

本機記憶體快取

如果未在您的設定檔中指定另一個快取,這是預設的快取。如果您想要記憶體內快取的速度優勢,但沒有執行 Memcached 的能力,請考慮使用本機記憶體快取後端。此快取是按進程(請參閱下文)且具有執行緒安全性的。若要使用它,請將 BACKEND 設定為 "django.core.cache.backends.locmem.LocMemCache"。例如

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
        "LOCATION": "unique-snowflake",
    }
}

快取 LOCATION 用於識別個別的記憶體儲存區。如果您只有一個 locmem 快取,您可以省略 LOCATION;但是,如果您有多個本機記憶體快取,您將需要為其中至少一個快取指派名稱,以使其保持分開。

快取使用最近最少使用 (LRU) 刪除策略。

請注意,每個進程都會有自己的私有快取實例,這表示無法進行跨進程快取。這也表示本機記憶體快取並不是特別節省記憶體,因此它可能不是生產環境的良好選擇。它在開發上很不錯。

虛擬快取(用於開發)

最後,Django 隨附一個「虛擬」快取,它實際上不快取,它只是實作快取介面而不做任何事情。

如果您有一個在各個地方使用大量快取的生產網站,但有一個開發/測試環境,您不想快取,也不想必須更改程式碼以特殊處理後者,這會很有用。若要啟動虛擬快取,請將 BACKEND 設定如下

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.dummy.DummyCache",
    }
}

使用自訂快取後端

雖然 Django 包含對許多開箱即用的快取後端的支援,但有時您可能想要使用自訂的快取後端。若要將外部快取後端與 Django 搭配使用,請使用 Python 匯入路徑作為 BACKENDCACHES 設定,如下所示

CACHES = {
    "default": {
        "BACKEND": "path.to.backend",
    }
}

如果您要建立自己的後端,您可以將標準快取後端作為參考實作。您會在 Django 原始碼的 django/core/cache/backends/ 目錄中找到程式碼。

注意:如果沒有真正令人信服的理由,例如主機不支援它們,您應該堅持使用 Django 隨附的快取後端。它們已經過充分測試且有完善的文件。

快取引數

可以為每個快取後端提供額外的引數,以控制快取行為。這些引數會以 CACHES 設定中的額外索引鍵提供。有效的引數如下

  • TIMEOUT:用於快取的預設逾時時間(以秒為單位)。此引數預設為 300 秒(5 分鐘)。您可以將 TIMEOUT 設定為 None,以便預設情況下快取索引鍵永不過期。值 0 會導致索引鍵立即過期(實際上是「不快取」)。

  • OPTIONS:應傳遞至快取後端的任何選項。有效選項的清單會因每個後端而異,且由第三方程式庫支援的快取後端會將其選項直接傳遞至底層快取程式庫。

    實作自己的刪除策略的快取後端(即 locmemfilesystemdatabase 後端)會遵循下列選項

    • MAX_ENTRIES:在刪除舊值之前,快取中允許的最大條目數。此引數預設為 300

    • CULL_FREQUENCY:當達到 MAX_ENTRIES 時要刪除的條目比例。實際比例為 1 / CULL_FREQUENCY,因此將 CULL_FREQUENCY 設定為 2,表示當達到 MAX_ENTRIES 時刪除一半的條目。此引數應為整數,預設值為 3

      CULL_FREQUENCY 的值為 0 時,表示當達到 MAX_ENTRIES 時將會清空整個快取。在某些後端(尤其是 database),這樣做會使刪除速度快得多,但代價是快取未命中率會更高。

    Memcached 和 Redis 後端會將 OPTIONS 的內容作為關鍵字引數傳遞給用戶端建構函式,以便對用戶端行為進行更進階的控制。如需範例用法,請參閱下文。

  • KEY_PREFIX:一個字串,將會自動包含(預設為前置)在 Django 伺服器使用的所有快取鍵中。

    如需更多資訊,請參閱快取文件

  • VERSION:Django 伺服器產生的快取鍵的預設版本號碼。

    如需更多資訊,請參閱快取文件

  • KEY_FUNCTION:一個字串,包含一個點分隔的路徑,指向一個函式,該函式定義如何將前綴、版本和鍵組合成最終的快取鍵。

    如需更多資訊,請參閱快取文件

在此範例中,檔案系統後端配置了 60 秒的逾時時間和 1000 個項目的最大容量

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
        "LOCATION": "/var/tmp/django_cache",
        "TIMEOUT": 60,
        "OPTIONS": {"MAX_ENTRIES": 1000},
    }
}

以下是基於 pylibmc 的後端範例配置,該配置啟用二進位協定、SASL 驗證和 ketama 行為模式

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.memcached.PyLibMCCache",
        "LOCATION": "127.0.0.1:11211",
        "OPTIONS": {
            "binary": True,
            "username": "user",
            "password": "pass",
            "behaviors": {
                "ketama": True,
            },
        },
    }
}

以下是基於 pymemcache 的後端範例配置,該配置啟用用戶端池 (這可以透過保持用戶端連線來提高效能)、將 memcache/網路錯誤視為快取未命中,並在連線的 socket 上設定 TCP_NODELAY 標誌

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache",
        "LOCATION": "127.0.0.1:11211",
        "OPTIONS": {
            "no_delay": True,
            "ignore_exc": True,
            "max_pool_size": 4,
            "use_pooling": True,
        },
    }
}

以下是基於 redis 的後端範例配置,該配置選擇資料庫 10 (預設情況下 Redis 會提供 16 個邏輯資料庫),並設定自訂的 連線池類別(預設情況下使用 redis.ConnectionPool

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.redis.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379",
        "OPTIONS": {
            "db": "10",
            "pool_class": "redis.BlockingConnectionPool",
        },
    }
}

每個網站的快取

設定好快取後,使用快取的最簡單方式是快取整個網站。您需要將 'django.middleware.cache.UpdateCacheMiddleware''django.middleware.cache.FetchFromCacheMiddleware' 加入您的 MIDDLEWARE 設定中,如下列範例所示

MIDDLEWARE = [
    "django.middleware.cache.UpdateCacheMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.cache.FetchFromCacheMiddleware",
]

注意

不,這不是筆誤:「更新」中介軟體必須位於列表的第一個,「提取」中介軟體必須位於最後一個。詳細資訊有點隱晦,但如果您想了解完整的故事,請參閱下面的中介軟體的順序

然後,將下列必要的設定新增至您的 Django 設定檔中

  • CACHE_MIDDLEWARE_ALIAS – 用於儲存的快取別名。

  • CACHE_MIDDLEWARE_SECONDS – 每個頁面應快取的整數秒數。

  • CACHE_MIDDLEWARE_KEY_PREFIX – 如果快取是在使用相同 Django 安裝的多個網站之間共用,請將此設定為網站名稱或其他對此 Django 執行個體而言獨一無二的字串,以防止鍵衝突。如果您不在意,請使用空字串。

FetchFromCacheMiddleware 會快取狀態碼為 200 的 GET 和 HEAD 回應,其中請求和回應標頭允許快取。具有不同查詢參數的相同 URL 請求的回應會被視為獨特的頁面,並會個別快取。此中介軟體預期 HEAD 請求的回應標頭與對應的 GET 請求相同;在這種情況下,它可以針對 HEAD 請求傳回快取的 GET 回應。

此外,UpdateCacheMiddleware 會自動在每個 HttpResponse 中設定幾個標頭,這些標頭會影響下游快取

如需關於中介軟體的詳細資訊,請參閱中介軟體

如果視圖設定了自身的快取過期時間 (亦即,其 Cache-Control 標頭中具有 max-age 區段),則頁面會快取到過期時間,而不是 CACHE_MIDDLEWARE_SECONDS。透過使用 django.views.decorators.cache 中的裝飾器,您可以輕鬆設定視圖的過期時間 (使用 cache_control() 裝飾器) 或停用視圖的快取 (使用 never_cache() 裝飾器)。如需關於這些裝飾器的詳細資訊,請參閱使用其他標頭區段。

如果 USE_I18N 設定為 True,則產生的快取鍵將包含有效語言的名稱 – 另請參閱Django 如何探索語言偏好。這可讓您輕鬆快取多語網站,而不必自行建立快取鍵。

USE_TZ 設定為 True 時,快取鍵也會包含目前時區

每個視圖的快取

django.views.decorators.cache.cache_page(timeout, *, cache=None, key_prefix=None)

使用快取框架更細緻的方式是快取個別視圖的輸出。django.views.decorators.cache 定義了一個 cache_page 裝飾器,該裝飾器會自動為您快取視圖的回應

from django.views.decorators.cache import cache_page


@cache_page(60 * 15)
def my_view(request): ...

cache_page 接受單一引數:快取逾時時間,以秒為單位。在上面的範例中,my_view() 視圖的結果將快取 15 分鐘。(請注意,為了易於閱讀,我們將其寫為 60 * 1560 * 15 會評估為 900 – 也就是每分鐘 60 秒乘以 15 分鐘。)

cache_page 設定的快取逾時時間優先於 Cache-Control 標頭中的 max-age 指令。

每個視圖的快取 (與每個網站的快取一樣) 是以 URL 作為鍵。如果多個 URL 指向同一個視圖,則每個 URL 都會個別快取。繼續 my_view 的範例,如果您的 URLconf 如下所示

urlpatterns = [
    path("foo/<int:code>/", my_view),
]

那麼對 /foo/1//foo/23/ 的請求將會個別快取,這符合您的預期。但是,一旦請求了特定的 URL (例如 /foo/23/),後續對該 URL 的請求就會使用快取。

cache_page 也可以接收一個選用的關鍵字參數 cache,這個參數會指示裝飾器使用特定的快取(來自你的 CACHES 設定)來快取視圖結果。預設情況下,會使用 default 快取,但你可以指定任何你想要的快取。

@cache_page(60 * 15, cache="special_cache")
def my_view(request): ...

你也可以在每個視圖的基礎上覆蓋快取前綴。cache_page 接收一個選用的關鍵字參數 key_prefix,它的運作方式與中介軟體的 CACHE_MIDDLEWARE_KEY_PREFIX 設定相同。它可以這樣使用:

@cache_page(60 * 15, key_prefix="site1")
def my_view(request): ...

key_prefixcache 參數可以一起指定。key_prefix 參數和在 CACHES 下指定的 KEY_PREFIX 將會被串連起來。

此外,cache_page 會自動在回應中設定 Cache-ControlExpires 標頭,這些標頭會影響 下游快取

在 URLconf 中指定每個視圖的快取

前一節的範例硬式編碼了視圖已被快取的事實,因為 cache_page 會就地修改 my_view 函數。這種方法將你的視圖與快取系統耦合在一起,這在幾個方面來說並不理想。例如,你可能想在另一個沒有快取的網站上重複使用視圖函數,或者你可能想將視圖分發給可能想在沒有快取的情況下使用它們的人。這些問題的解決方案是在 URLconf 中指定每個視圖的快取,而不是在視圖函數本身旁邊指定。

你可以透過在 URLconf 中引用視圖函數時,使用 cache_page 包裝它來做到這一點。這是稍早的舊 URLconf:

urlpatterns = [
    path("foo/<int:code>/", my_view),
]

這是相同的內容,但 my_view 被包在 cache_page 中:

from django.views.decorators.cache import cache_page

urlpatterns = [
    path("foo/<int:code>/", cache_page(60 * 15)(my_view)),
]

樣板片段快取

如果你想要更多的控制權,你也可以使用 cache 樣板標籤來快取樣板片段。要讓你的樣板能夠使用這個標籤,請將 {% load cache %} 放在你的樣板頂部附近。

{% cache %} 樣板標籤會將區塊的內容快取一段指定的時間。它至少需要兩個參數:快取逾時時間(以秒為單位)和快取片段的名稱。如果逾時時間是 None,則該片段將永遠被快取。名稱將按原樣採用,不要使用變數。例如:

{% load cache %}
{% cache 500 sidebar %}
    .. sidebar ..
{% endcache %}

有時,你可能希望根據出現在片段內的一些動態資料來快取片段的多個副本。例如,你可能希望為你的網站的每個使用者都使用之前範例中使用的側邊欄的單獨快取副本。你可以透過將一個或多個附加參數(可以是帶有或不帶有篩選器的變數)傳遞給 {% cache %} 樣板標籤,以唯一識別快取片段,來達成此目的:

{% load cache %}
{% cache 500 sidebar request.user.username %}
    .. sidebar for logged in user ..
{% endcache %}

如果 USE_I18N 設定為 True,則每個網站的中介軟體快取會考量作用中的語言。對於 cache 樣板標籤,你可以使用樣板中可用的特定於翻譯的變數之一,來達成相同的結果。

{% load i18n %}
{% load cache %}

{% get_current_language as LANGUAGE_CODE %}

{% cache 600 welcome LANGUAGE_CODE %}
    {% translate "Welcome to example.com" %}
{% endcache %}

快取逾時時間可以是樣板變數,只要該樣板變數解析為整數值即可。例如,如果樣板變數 my_timeout 設定為值 600,則以下兩個範例是等效的:

{% cache 600 sidebar %} ... {% endcache %}
{% cache my_timeout sidebar %} ... {% endcache %}

此功能在避免樣板中的重複方面很有用。你可以在一個地方的變數中設定逾時時間,並重複使用該值。

預設情況下,快取標籤會嘗試使用名為「template_fragments」的快取。如果不存在這樣的快取,它將會退回到使用預設快取。你可以使用 using 關鍵字參數來選擇要使用的替代快取後端,它必須是標籤的最後一個參數。

{% cache 300 local-thing ...  using="localcache" %}

指定未設定的快取名稱被認為是錯誤。

django.core.cache.utils.make_template_fragment_key(fragment_name, vary_on=None)

如果你想取得用於快取片段的快取鍵,你可以使用 make_template_fragment_keyfragment_namecache 樣板標籤的第二個參數相同;vary_on 是傳遞給該標籤的所有附加參數的清單。例如,此函數可用於使快取項目失效或覆寫快取項目:

>>> from django.core.cache import cache
>>> from django.core.cache.utils import make_template_fragment_key
# cache key for {% cache 500 sidebar username %}
>>> key = make_template_fragment_key("sidebar", [username])
>>> cache.delete(key)  # invalidates cached template fragment
True

低階快取 API

有時,快取整個渲染頁面並不會為你帶來太大的好處,而且實際上是一種不方便的過度處理。

例如,你的網站可能包含一個視圖,其結果取決於幾個耗費資源的查詢,這些查詢的結果會以不同的時間間隔變更。在這種情況下,使用每個網站或每個視圖快取策略提供的完整頁面快取並不是理想的做法,因為你不會想要快取整個結果(因為某些資料會經常變更),但你仍然想要快取很少變更的結果。

對於這種情況,Django 公開了一個低階快取 API。你可以使用這個 API,以你喜歡的任何細微程度將物件儲存在快取中。你可以快取任何可以安全 pickle 的 Python 物件:字串、字典、模型物件清單等等。(大多數常見的 Python 物件都可以 pickle;有關 pickle 的更多資訊,請參閱 Python 文件。)

存取快取

django.core.cache.caches

你可以透過類似字典的物件:django.core.cache.caches,存取在 CACHES 設定中設定的快取。在同一個執行緒中重複請求相同的別名,將會傳回相同的物件。

>>> from django.core.cache import caches
>>> cache1 = caches["myalias"]
>>> cache2 = caches["myalias"]
>>> cache1 is cache2
True

如果指定的鍵不存在,則會引發 InvalidCacheBackendError

為了提供執行緒安全,每個執行緒都會傳回快取後端不同的執行個體。

django.core.cache.cache

作為捷徑,預設快取可以 django.core.cache.cache 取得。

>>> from django.core.cache import cache

這個物件等同於 caches['default']

基本用法

基本介面是:

cache.set(key, value, timeout=DEFAULT_TIMEOUT, version=None)
>>> cache.set("my_key", "hello, world!", 30)
cache.get(key, default=None, version=None)
>>> cache.get("my_key")
'hello, world!'

key 應該是 str,而 value 可以是任何可 pickle 的 Python 物件。

timeout 引數是選用的,預設為 CACHES 設定中適當後端的 timeout 引數(如上所述)。這是該值應儲存在快取中的秒數。為 timeout 傳入 None 將會永遠快取該值。為 timeout 傳入 0 將不會快取該值。

如果物件不存在於快取中,cache.get() 會傳回 None

>>> # Wait 30 seconds for 'my_key' to expire...
>>> cache.get("my_key")
None

如果你需要確定物件是否存在於快取中,並且你已經儲存了常值 None,請使用哨兵物件作為預設值:

>>> sentinel = object()
>>> cache.get("my_key", sentinel) is sentinel
False
>>> # Wait 30 seconds for 'my_key' to expire...
>>> cache.get("my_key", sentinel) is sentinel
True

cache.get() 可以接收 default 引數。如果物件不存在於快取中,這會指定要傳回哪個值:

>>> cache.get("my_key", "has expired")
'has expired'
cache.add(key, value, timeout=DEFAULT_TIMEOUT, version=None)

若要僅當某個鍵不存在時才新增它,請使用 add() 方法。它會接收與 set() 相同的參數,但如果指定的鍵已存在,它不會嘗試更新快取:

>>> cache.set("add_key", "Initial value")
>>> cache.add("add_key", "New value")
>>> cache.get("add_key")
'Initial value'

如果您需要知道 add() 是否將值儲存在快取中,您可以檢查其回傳值。如果該值已儲存,它會回傳 True,否則回傳 False

cache.get_or_set(key, default, timeout=DEFAULT_TIMEOUT, version=None)

如果您想取得鍵的值,或是在快取中沒有該鍵時設定一個值,可以使用 get_or_set() 方法。它接受與 get() 相同的參數,但是預設值會設定為該鍵的新快取值,而不是回傳。

>>> cache.get("my_new_key")  # returns None
>>> cache.get_or_set("my_new_key", "my new value", 100)
'my new value'

您也可以傳遞任何可呼叫的物件作為預設值。

>>> import datetime
>>> cache.get_or_set("some-timestamp-key", datetime.datetime.now)
datetime.datetime(2014, 12, 11, 0, 15, 49, 457920)
cache.get_many(keys, version=None)

還有一個 get_many() 介面,它只會存取快取一次。get_many() 會回傳一個字典,其中包含您要求的所有實際存在於快取中(且尚未過期)的鍵。

>>> cache.set("a", 1)
>>> cache.set("b", 2)
>>> cache.set("c", 3)
>>> cache.get_many(["a", "b", "c"])
{'a': 1, 'b': 2, 'c': 3}
cache.set_many(dict, timeout)

若要更有效率地設定多個值,請使用 set_many() 傳遞鍵值對的字典。

>>> cache.set_many({"a": 1, "b": 2, "c": 3})
>>> cache.get_many(["a", "b", "c"])
{'a': 1, 'b': 2, 'c': 3}

cache.set() 類似,set_many() 接受可選的 timeout 參數。

在支援的後端(memcached)上,set_many() 會回傳一個無法插入的鍵列表。

cache.delete(key, version=None)

您可以使用 delete() 明確刪除鍵,以清除特定物件的快取。

>>> cache.delete("a")
True

如果成功刪除鍵,delete() 會回傳 True,否則回傳 False

cache.delete_many(keys, version=None)

如果您想一次清除多個鍵,delete_many() 可以接受要清除的鍵列表。

>>> cache.delete_many(["a", "b", "c"])
cache.clear()

最後,如果您想刪除快取中的所有鍵,請使用 cache.clear()。請謹慎使用此方法;clear() 會從快取中移除所有內容,而不僅僅是您的應用程式設定的鍵。

>>> cache.clear()
cache.touch(key, timeout=DEFAULT_TIMEOUT, version=None)

cache.touch() 會為鍵設定新的到期時間。例如,若要將鍵更新為從現在起 10 秒後到期:

>>> cache.touch("a", 10)
True

與其他方法一樣,timeout 參數是可選的,預設為 CACHES 設定中適當後端的 TIMEOUT 選項。

如果成功觸摸鍵,touch() 會回傳 True,否則回傳 False

cache.incr(key, delta=1, version=None)
cache.decr(key, delta=1, version=None)

您也可以分別使用 incr()decr() 方法,遞增或遞減已存在的鍵。預設情況下,現有的快取值會遞增或遞減 1。您可以透過在遞增/遞減呼叫中提供參數來指定其他遞增/遞減值。如果您嘗試遞增或遞減不存在的快取鍵,則會引發 ValueError。

>>> cache.set("num", 1)
>>> cache.incr("num")
2
>>> cache.incr("num", 10)
12
>>> cache.decr("num")
11
>>> cache.decr("num", 5)
6

注意

incr()/ decr() 方法不能保證是原子性的。在支援原子遞增/遞減的後端(最明顯的是 memcached 後端)上,遞增和遞減操作將是原子性的。但是,如果後端本身不提供遞增/遞減操作,它將使用兩步驟的擷取/更新來實作。

cache.close()

如果快取後端實作了 close(),您可以使用 close() 關閉與快取的連線。

>>> cache.close()

注意

對於未實作 close 方法的快取,它是一個空操作。

注意

基本方法的非同步變體會加上 a 前綴,例如 cache.aadd()cache.adelete_many()。有關更多詳細資訊,請參閱非同步支援

快取鍵前綴

如果您在伺服器之間,或在您的生產和開發環境之間共享快取實例,則有可能一個伺服器快取的資料被另一個伺服器使用。如果伺服器之間快取資料的格式不同,則可能導致一些很難診斷的問題。

為了防止這種情況,Django 提供了為伺服器使用的所有快取鍵加上前綴的功能。當儲存或擷取特定快取鍵時,Django 會自動使用 KEY_PREFIX 快取設定的值為快取鍵加上前綴。

透過確保每個 Django 實例都有不同的 KEY_PREFIX,您可以確保快取值中不會發生衝突。

快取版本控制

當您變更使用快取值的執行程式碼時,您可能需要清除任何現有的快取值。最簡單的方法是刷新整個快取,但這可能會導致遺失仍然有效且有用的快取值。

Django 提供了一種更好的方法來鎖定個別快取值。Django 的快取框架有一個系統範圍的版本識別碼,使用 VERSION 快取設定指定。此設定的值會自動與快取前綴和使用者提供的快取鍵組合,以取得最終快取鍵。

預設情況下,任何鍵請求都會自動包含站點預設快取鍵版本。但是,原始快取函數都包含 version 參數,因此您可以指定要設定或取得的特定快取鍵版本。例如:

>>> # Set version 2 of a cache key
>>> cache.set("my_key", "hello world!", version=2)
>>> # Get the default version (assuming version=1)
>>> cache.get("my_key")
None
>>> # Get version 2 of the same key
>>> cache.get("my_key", version=2)
'hello world!'

可以使用 incr_version()decr_version() 方法來遞增和遞減特定鍵的版本。這使得可以將特定鍵升級到新版本,而不會影響其他鍵。繼續前面的例子:

>>> # Increment the version of 'my_key'
>>> cache.incr_version("my_key")
>>> # The default version still isn't available
>>> cache.get("my_key")
None
# Version 2 isn't available, either
>>> cache.get("my_key", version=2)
None
>>> # But version 3 *is* available
>>> cache.get("my_key", version=3)
'hello world!'

快取鍵轉換

如前兩節所述,使用者提供的快取鍵不會被逐字使用 – 它會與快取前綴和鍵版本組合,以提供最終快取鍵。預設情況下,這三個部分會使用冒號連接以產生最終字串。

def make_key(key, key_prefix, version):
    return "%s:%s:%s" % (key_prefix, version, key)

如果您想以不同的方式組合這些部分,或對最終鍵應用其他處理(例如,取得鍵部分的雜湊摘要),則可以提供自訂鍵函數。

KEY_FUNCTION 快取設定指定一個符合上述 make_key() 原型的函數的點式路徑。如果提供,此自訂鍵函數將會取代預設的鍵組合函數。

快取鍵警告

Memcached 是最常用的生產快取後端,不允許快取鍵超過 250 個字元或包含空白字元或控制字元,使用此類鍵會導致例外。為了鼓勵可移植的快取程式碼並盡量減少令人不快的意外,如果使用的鍵會在 memcached 上造成錯誤,其他內建快取後端會發出警告 (django.core.cache.backends.base.CacheKeyWarning)。

如果您使用的是可以接受更廣泛鍵值的生產環境後端(自訂後端,或非快取記憶體內建後端之一),並且希望在不出現警告的情況下使用此更廣泛的範圍,您可以使用以下程式碼在您的其中一個 INSTALLED_APPSmanagement 模組中關閉 CacheKeyWarning

import warnings

from django.core.cache import CacheKeyWarning

warnings.simplefilter("ignore", CacheKeyWarning)

如果您想要為其中一個內建後端提供自訂的鍵值驗證邏輯,您可以繼承它,僅覆寫 validate_key 方法,並遵循使用自訂快取後端的說明。例如,若要對 locmem 後端執行此操作,請將此程式碼放在模組中

from django.core.cache.backends.locmem import LocMemCache


class CustomLocMemCache(LocMemCache):
    def validate_key(self, key):
        """Custom validation, raising exceptions or warnings as needed."""
        ...

…並在您的 CACHES 設定的 BACKEND 部分中使用此類別的虛線 Python 路徑。

非同步支援

Django 正在開發對非同步快取後端的支援,但尚未支援非同步快取。它將在未來的版本中推出。

django.core.cache.backends.base.BaseCache 具有所有基本方法的非同步變體。按照慣例,所有方法的非同步版本都以 a 作為字首。預設情況下,兩種變體的參數相同

>>> await cache.aset("num", 1)
>>> await cache.ahas_key("num")
True

下游快取

到目前為止,本文檔的重點是快取您自己的資料。但是,另一種快取類型也與網頁開發相關:由「下游」快取執行的快取。這些系統會在請求到達您的網站之前就為使用者快取頁面。

以下是一些下游快取的範例

  • 使用 HTTP 時,您的 ISP 可能會快取某些頁面,因此如果您從 http://example.com/ 請求頁面,您的 ISP 會將頁面傳送給您,而無需直接存取 example.com。example.com 的維護人員不知道此快取;ISP 位於 example.com 和您的網頁瀏覽器之間,透明地處理所有快取。HTTPS 下無法進行此類快取,因為這會構成中間人攻擊。

  • 您的 Django 網站可能位於代理快取之後,例如 Squid 網頁代理快取(http://www.squid-cache.org/),該快取會為了效能而快取頁面。在這種情況下,每個請求都會先由代理處理,並且僅在需要時才會傳遞到您的應用程式。

  • 您的網頁瀏覽器也會快取頁面。如果網頁發送適當的標頭,您的瀏覽器將使用本機快取的副本來處理對該頁面的後續請求,甚至不會再次聯絡網頁以查看是否已變更。

下游快取是一種很好的效率提升,但它也存在危險:許多網頁的內容會根據身分驗證和許多其他變數而有所不同,而僅根據 URL 盲目儲存頁面的快取系統可能會向該頁面的後續訪客暴露不正確或敏感的資料。

例如,如果您經營一個網頁電子郵件系統,則「收件匣」頁面的內容取決於哪個使用者已登入。如果 ISP 盲目地快取您的網站,則第一個透過該 ISP 登入的使用者會將他們的使用者專屬收件匣頁面快取給該網站的後續訪客。這可不好。

幸運的是,HTTP 為這個問題提供了解決方案。存在許多 HTTP 標頭,指示下游快取根據指定的變數區分其快取內容,並告訴快取機制不要快取特定的頁面。我們將在後續章節中介紹其中一些標頭。

使用 Vary 標頭

Vary 標頭定義了快取機制在建立其快取鍵時應考慮哪些請求標頭。例如,如果網頁的內容取決於使用者的語言偏好,則該頁面據說「因語言而異」。

預設情況下,Django 的快取系統會使用請求的完整 URL 來建立其快取鍵 – 例如 "https://www.example.com/stories/2005/?order_by=author"。這表示對該 URL 的每個請求都將使用相同的快取版本,無論使用者代理程式的差異(例如 Cookie 或語言偏好)如何。但是,如果此頁面根據請求標頭中的某些差異(例如 Cookie、語言或使用者代理程式)產生不同的內容,則您需要使用 Vary 標頭來告知快取機制頁面輸出取決於這些內容。

若要在 Django 中執行此操作,請使用方便的 django.views.decorators.vary.vary_on_headers() 檢視裝飾器,如下所示

from django.views.decorators.vary import vary_on_headers


@vary_on_headers("User-Agent")
def my_view(request): ...

在這種情況下,快取機制(例如 Django 自己的快取中介軟體)將為每個唯一的使用者代理程式快取一個單獨的頁面版本。

使用 vary_on_headers 裝飾器而不是手動設定 Vary 標頭(使用類似 response.headers['Vary'] = 'user-agent' 的方法)的優點是,裝飾器會新增Vary 標頭(可能已存在),而不是從頭開始設定它並可能覆寫其中已有的任何內容。

您可以將多個標頭傳遞到 vary_on_headers()

@vary_on_headers("User-Agent", "Cookie")
def my_view(request): ...

這會告知下游快取在兩者上變化,這表示每個使用者代理程式和 Cookie 的組合都會獲得自己的快取值。例如,使用者代理程式為 Mozilla 且 Cookie 值為 foo=bar 的請求將被視為與使用者代理程式為 Mozilla 且 Cookie 值為 foo=ham 的請求不同。

因為在 Cookie 上變化很常見,所以有一個 django.views.decorators.vary.vary_on_cookie() 裝飾器。這兩個檢視是等效的

@vary_on_cookie
def my_view(request): ...


@vary_on_headers("Cookie")
def my_view(request): ...

您傳遞到 vary_on_headers 的標頭不區分大小寫;"User-Agent""user-agent" 相同。

您也可以直接使用協助程式函式 django.utils.cache.patch_vary_headers()。此函式會設定或新增到 Vary header。例如

from django.shortcuts import render
from django.utils.cache import patch_vary_headers


def my_view(request):
    ...
    response = render(request, "template_name", context)
    patch_vary_headers(response, ["Cookie"])
    return response

patch_vary_headersHttpResponse 執行個體作為其第一個引數,並將不區分大小寫的標頭名稱清單/元組作為其第二個引數。

如需有關 Vary 標頭的詳細資訊,請參閱 官方 Vary 規格

控制快取:使用其他標頭

快取的其他問題是資料的隱私權,以及資料應儲存在快取階層中的哪個位置。

使用者通常會遇到兩種快取:他們自己的瀏覽器快取(私有快取)和他們供應商的快取(公用快取)。公用快取由多個使用者使用,並由其他人控制。這會對敏感資料造成問題–您不希望將您的銀行帳戶號碼儲存在公用快取中。因此,網頁應用程式需要一種方法來告知快取哪些資料是私有的,哪些是公用的。

解決方案是指示頁面的快取應為「私有」。若要在 Django 中執行此操作,請使用 cache_control() 檢視裝飾器。範例

from django.views.decorators.cache import cache_control


@cache_control(private=True)
def my_view(request): ...

此裝飾器會負責在幕後發送適當的 HTTP 標頭。

請注意,快取控制設定「private」和「public」是互斥的。如果應該設定「private」,則裝飾器會確保移除「public」指令(反之亦然)。這兩個指令的一個範例用途是提供私有和公用項目的部落格網站。公用項目可能會在任何共用快取上快取。以下程式碼使用 patch_cache_control(),這是手動修改快取控制標頭的方法(它在內部由 cache_control() 裝飾器呼叫)

from django.views.decorators.cache import patch_cache_control
from django.views.decorators.vary import vary_on_cookie


@vary_on_cookie
def list_blog_entries_view(request):
    if request.user.is_anonymous:
        response = render_only_public_entries()
        patch_cache_control(response, public=True)
    else:
        response = render_private_and_public_entries(request.user)
        patch_cache_control(response, private=True)

    return response

您也可以透過其他方式控制下游快取(有關 HTTP 快取的詳細資訊,請參閱 RFC 9111)。例如,即使您不使用 Django 的伺服器端快取架構,您仍然可以使用 max-age 指令,告知用戶端快取檢視一段時間

from django.views.decorators.cache import cache_control


@cache_control(max_age=3600)
def my_view(request): ...

(如果您確實使用快取中介軟體,它已經使用 CACHE_MIDDLEWARE_SECONDS 設定的值設定了 max-age。在這種情況下,來自 cache_control() 裝飾器的自訂 max_age 將優先,並且標頭值將正確合併。)

任何有效的 Cache-Control 回應指令在 cache_control() 中都有效。以下是一些更多範例

  • no_transform=True

  • must_revalidate=True

  • stale_while_revalidate=num_seconds

  • no_cache=True

已知的所有指令完整列表可以在 IANA 註冊表 中找到(請注意,並非所有指令都適用於回應)。

如果您想使用標頭來完全停用快取,never_cache() 是一個視圖裝飾器,它會加入標頭以確保回應不會被瀏覽器或其他快取所快取。範例:

from django.views.decorators.cache import never_cache


@never_cache
def myview(request): ...

MIDDLEWARE 的順序

如果您使用快取中介軟體,請務必將每一半放在 MIDDLEWARE 設定中的正確位置。這是因為快取中介軟體需要知道依據哪些標頭來改變快取儲存。中介軟體在可以時,總是在回應標頭中加入一些內容到 Vary

UpdateCacheMiddleware 在回應階段執行,其中中介軟體以相反順序執行,因此列表頂端的項目在回應階段會最後執行。因此,您需要確保 UpdateCacheMiddleware 出現在任何可能在 Vary 標頭中加入內容的其他中介軟體之前。以下中介軟體模組會這樣做:

  • SessionMiddleware 加入 Cookie

  • GZipMiddleware 加入 Accept-Encoding

  • LocaleMiddleware 加入 Accept-Language

另一方面,FetchFromCacheMiddleware 在請求階段執行,其中中介軟體以先到後到的順序應用,因此列表頂端的項目在請求階段會最先執行。FetchFromCacheMiddleware 也需要在其他中介軟體更新 Vary 標頭之後執行,因此 FetchFromCacheMiddleware 必須在任何執行此操作的項目之後

返回頂部