Django 缓存框架

动态网站的一个基本权衡是,它们是动态的。每次用户请求一个页面时,Web 服务器都会进行各种计算——从数据库查询到模板渲染到业务逻辑——来创建网站访问者看到的页面。从处理开销的角度来看,这比标准的从文件系统读取文件的服务器安排要昂贵得多。

对于大多数 Web 应用程序来说,这种开销并不算什么。大多数 Web 应用程序都不是 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 和维基百科等网站使用它来减少数据库访问并显著提高网站性能。

Memcached 作为守护进程运行,并分配给定的内存量。它所做的只是提供一个快速接口来添加、检索和删除缓存中的数据。所有数据都直接存储在内存中,因此没有数据库或文件系统使用的开销。

在安装 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 套接字文件的路径。

在此示例中,Memcached 在 localhost (127.0.0.1) 端口 11211 上运行,使用 pymemcache 绑定

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

在此示例中,Memcached 通过本地 Unix 套接字文件 /tmp/memcached.sock 使用 pymemcache 绑定可用

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 的 Memcached 实例上共享,两者都在端口 11211 上

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 文档 details on the available schemes

例如,如果 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",
    }
}

目录路径应为绝对路径,即应从文件系统的根目录开始。在设置的末尾添加斜杠与否无关紧要。

确保此设置指向的目录要么存在且可读可写,要么可以由您的 Web 服务器运行的用户创建。继续上面的示例,如果您的服务器以用户 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/网络错误视为缓存未命中,并在连接的套接字上设置 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",
]

注意

不,这不是错字:“更新”中间件必须位于列表的开头,“获取”中间件必须位于末尾。详细信息有点模糊,但如果您想了解完整的故事,请参阅下面的 MIDDLEWARE 的顺序

然后,将以下必需设置添加到您的 Django settings 文件中

  • 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 * 15 以提高可读性。60 * 15 将计算为 900 - 即 15 分钟乘以每分钟 60 秒。)

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),
]

这是相同的内容,使用cache_page包装了my_view

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

您可以通过类似字典的对象访问CACHES设置中配置的缓存:django.core.cache.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 应为strvalue可以是任何可 pickle 的 Python 对象。

timeout参数是可选的,默认为CACHES设置中相应后端的timeout参数(如上所述)。它是值应在缓存中存储的秒数。将None传递给timeout将永远缓存该值。timeout0将不会缓存该值。

如果对象在缓存中不存在,则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'

您还可以传递任何可调用对象作为 default 值。

>>> 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() 方法,您可以使用它关闭与缓存的连接。

>>> 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)。

如果使用的是可以接受更广泛的键范围的生产后端(自定义后端或非 Memcached 内置后端之一),并且希望在不发出警告的情况下使用此更广泛的范围,则可以在其中一个 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."""
        ...

…并在 BACKEND 部分的 CACHES 设置中使用此类的点分 Python 路径。

异步支持

Django 正在开发对异步缓存后端的支持,但尚不支持异步缓存。这将在未来的版本中提供。

django.core.cache.backends.base.BaseCache 具有 所有基本方法 的异步变体。按照惯例,所有方法的异步版本都以 a 为前缀。默认情况下,两种变体的参数相同。

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

下游缓存

到目前为止,本文档重点介绍了缓存您自己的数据。但是,另一种类型的缓存也与 Web 开发相关:由“下游”缓存执行的缓存。这些是在请求到达您的网站之前甚至为用户缓存页面的系统。

以下是一些下游缓存的示例。

  • 使用 HTTP 时,您的 ISP 可能会缓存某些页面,因此,如果您从 http://example.com/ 请求页面,您的 ISP 将向您发送该页面,而无需直接访问 example.com。example.com 的维护人员不知道此缓存;ISP 位于 example.com 和您的 Web 浏览器之间,透明地处理所有缓存。由于这构成中间人攻击,因此在 HTTPS 下无法进行此类缓存。

  • 您的 Django 网站可能位于代理缓存(例如 Squid Web Proxy Cache (http://www.squid-cache.org/))之后,该缓存缓存页面以提高性能。在这种情况下,每个请求首先将由代理处理,并且仅在需要时才会传递到您的应用程序。

  • 您的 Web 浏览器也会缓存页面。如果网页发送了相应的标头,则您的浏览器将在后续请求该页面时使用本地缓存的副本,甚至不会再次联系网页以查看它是否已更改。

下游缓存可以很好地提高效率,但它也存在风险:许多网页的内容根据身份验证和其他各种变量而有所不同,并且仅根据 URL 盲目保存页面的缓存系统可能会向后续访问这些页面的访问者公开不正确或敏感的数据。

例如,如果您运营一个 Web 邮件系统,则“收件箱”页面的内容取决于登录的用户。如果 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 规范

控制缓存:使用其他标头

缓存的其他问题是数据的隐私以及数据应在缓存级联中存储在何处的问题。

用户通常会遇到两种缓存:他们自己的浏览器缓存(私有缓存)和他们提供商的缓存(公共缓存)。公共缓存由多个用户使用,并由其他人控制。这会带来敏感数据问题——例如,您不希望您的银行账户号码存储在公共缓存中。因此,Web 应用程序需要一种方法来告诉缓存哪些数据是私有的,哪些数据是公共的。

解决方案是指示页面的缓存应为“私有”。要在 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 必须位于执行此操作的任何项目的之后

返回顶部