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 标头提供提示的缓存类型,用于指示哪些部分的网站应该被缓存,以及如何缓存。

参见

缓存框架设计理念 解释了框架的一些设计决策。

设置缓存

缓存系统需要少量的设置。也就是说,你必须告诉它你的缓存数据应该放在哪里 —— 是在数据库中,还是在文件系统上,或者直接放在内存中。这是一个重要的决定,会影响你的缓存的性能;是的,有些缓存类型比其他类型快。

缓存设置项位于你的配置文件的缓存配置中。这里有缓存配置所有可用值的说明。

Memcached

Memcached 是一个完全基于内存的缓存服务器,最初是为了处理 LiveJournal.com 上的高负载而开发的,并随后由 Danga Interactive 开源。它被类似 Facebook 和 Wikipedia 这样的网站使用,以减少数据库访问并显著提高网站性能。

Memcached 以一个守护进程的形式运行,并且被分配了指定数量的 RAM。它所做的就是提供一个快速接口用于在缓存中添加,检索和删除数据。所有数据都直接存储在内存中,因此不会产生数据库或文件系统使用的开销。

在安装 Memcached 本身之后,您需要安装一个 Memcached 绑定。有几个 Python Memcached 绑定可用,Django 支持的两个是 pylibmcpymemcache

在 Django 中使用 Memcached :

  • 设置 BACKEND 为 django.core.cache.backends.memcached.PyMemcacheCache 或 django.core.cache.backends.memcached.PyLibMCCache (取决于你选择的 memcached 绑定)。
  • LOCATION 设置为 ip:port 值,其中 ip 是 Memcached 守护进程的 IP 地址,port 是 Memcached 运行的端口,或者设置为 unix:path 值,其中 path 是 Memcached Unix socket 文件的路径。

在这个例子中,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 绑定。Django 原生支持的绑定是 redis-py。同时,建议安装 hiredis-py 包。

要在 Django 中使用 Redis 作为缓存后端:

  • BACKEND 设置为 django.core.cache.backends.redis.RedisCache
  • LOCATION 设置为指向您的 Redis 实例的 URL,使用适当的方案。参见 redis-py 文档中关于 可用方案的详细信息

例如,如果 Redis 在本地主机(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 选项。

多数据库

如果在多数据库中使用缓存,你也需要设置数据库缓存表的路由指令。因为路由的原因,数据库缓存表在 django_cache 应用程序中显示为 CacheEntry 的模型名。这个模型不会出现在模型缓存中,但模型详情可用于路由目的。

比如,下面的路由可以将所有缓存读取操作指向 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

如果你没有指定路由指向数据库缓存模型,缓存后端将使用 默认 的数据库。

如果没使用数据库缓存后端,则无需担心为数据库缓存模型提供路由指令。

文件系统缓存

基于文件的后端序列化并保存每个缓存值作为单独的文件。要使用此后端,可将 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_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 配置中的 BACKEND,像这样:

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 个逻辑数据库),指定了一个 parser class (如果安装了 hiredis-py 包,则默认使用 redis.connection.HiredisParser),并设置了自定义的 connection pool class (默认情况下使用 redis.ConnectionPool):

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

站点缓存

一旦缓存设置完毕,使用缓存最简便的方式就是缓存整个站点。你需要在 MIDDLEWARE 设置中添加 'django.middleware.cache.UpdateCacheMiddleware''django.middleware.cache.FetchFromCacheMiddleware' ,像下面这个例子一样:

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

备注

不,这不是错别字:“update” 中间件必须在列表的第一位,而 “fetch” 中间件必须在最后。细节有点晦涩难懂,但如果你想知道完整的故事,请看下面的 中间件的顺序

最后,在 Django 设置文件里添加下面的必需配置:

  • CACHE_MIDDLEWARE_ALIAS -- 用于存储的缓存别名。
  • CACHE_MIDDLEWARE_SECONDS -- 应缓存每个页面的秒数。
  • CACHE_MIDDLEWARE_KEY_PREFIX -- 如果使用相同的 Django installation ,通过多站点进行缓存共享,请将此值设置为站点名,或者设置成在Django 实例中唯一的其他字符串,以此防止键冲突。如果你不介意,可以设置成空字符串。

在请求和响应标头允许的情况下,FetchFromCacheMiddleware 缓存状态为200的 GET 和 HEAD 响应。对于具有不同查询参数的相同URL的请求的响应被认为是单独的页面,并分别缓存。这个中间件期望一个HEAD请求的响应头与相应的GET请求具有相同的响应头;在这种情况下,它可以为HEAD请求返回一个缓存的GET响应。

此外,UpdateCacheMiddleware 在每个 HttpResponse 里会自动设置一些 headers,这会影响 下游缓存:

查看 中间件 获取更多中间件信息。

如果一个视图设置了它自己的缓存过期时间(比如在它的 Cache-Control header 里有 max-age 部分),然后页面将被缓存起来直到过期,而不是 CACHE_MIDDLEWARE_SECONDS 。使用在 django.views.decorators.cache 的装饰器,你可以很轻松的设置视图的过期时间(使用 cache_control() 装饰器)或者禁用视图缓存(使用 never_cache() 装饰器)。有关这些装饰器的更多信息,请查看 using other headers 部分。

如果设置 USE_I18NTrue,然后已生成的缓存键将包含动态 language 的名称(参阅 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/ )已经被请求,那么随后的请求都将使用缓存。

cache_page 也可以传递可选关键字参数 cache,它指引装饰器在缓存视图结果时使用特定的缓存(来自 CACHES 设置)。默认情况下,将使用默认缓存,但你可以指定任何你想要的缓存:

@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 模板标签(tag)来缓存模板片段。要使你的模板能够访问这个标签,请将 {% load cache %} 放在模板顶部。

{% cache %} 模板标签在给定的时间里缓存片段内容。它需要至少两个参数:缓存时效时间(以秒为单位),缓存片段的名称。如果缓存失效时间被设置为 None ,那么片段将被永久缓存。名称不能使变量名。例如:

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

有时你想缓存片段的多个副本,这取决于显示在的片段内一些动态数据。比如,你可能想为你的站点内每个用户分别独立缓存上面例子中的使用的 sidebar 副本。通过传递一个或多个附加参数,参数可能是带有或不带过滤器的变量,{% cache %} 模板标签必须在缓存片断中被唯一识别:

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

如果 USE_I18N 被设为 True,那么站点中间件缓存将支持多语言( respect the active language )。对于 cache 模板标签来说,你可以使用模板中可用的特定翻译变量之一( translation-specific variables )来达到同样的结果:

{% 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;可以参考 Python 文档关于 pickling 的信息)

访问缓存

django.core.cache.caches

你可以通过类似字典一样的 object: 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 是一个字符串,value 可以任何 picklable 形式的 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'

您还可以将任何可调用对象作为 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() ,否则返回 False

cache.delete_many(keys, version=None)

If you want to clear a bunch of keys at once, delete_many() can take a list of keys to be cleared:

>>> 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_APPS 中的 management 模块里静默 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 部分使用路径导入此类。

异步支持

Django 目前已经有了对异步缓存后端的支持,但尚未支持异步缓存。这将在将来的版本中推出。

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

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

下游缓存

到目前为止,本文档已经专注于缓存您自己的数据。但是,与 web 开发相关的另一种缓存是由 "下游" 缓存执行的。这些是在请求到达您的网站之前,为用户缓存页面的系统。

下面是一些下游缓存的例子:

  • When using HTTP, your ISP may cache certain pages, so if you requested a page from http://example.com/, your ISP would send you the page without having to access example.com directly. The maintainers of example.com have no knowledge of this caching; the ISP sits between example.com and your web browser, handling all of the caching transparently. Such caching is not possible under HTTPS as it would constitute a man-in-the-middle attack.
  • 您的 Django 网站可能会在一个*代理缓存*的后面,例如Squid 网页代理缓存(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 的每个请求都将使用相同的缓存版本,而不管用户代理差异(如 cookies 或语言首选项)。但是,如果这个页面基于请求头(如 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_headers 带有一个 HttpResponse 作为它的第一个参数,一个不区分大小写的头名的列表/元组作为它的第二个参数。

有关更多关于 Vary 标头的信息,请参阅 official Vary spec

使用其他标头控制高速缓存

缓存的其他问题是数据的隐私和数据应该存储在缓存的级联中的问题。

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

解决方案是指出一个页面的缓存应该是“私有的”。在 Django中,使用 cache_control() 。例子:

from django.views.decorators.cache import cache_control


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

这个装饰器负责在场景后面发送适当的 HTTP 头。

注意,缓存控制设置“私有”和“公共”是互斥的。装饰器确保“公共”指令被移除,如果应该设置“私有”(反之亦然)。这两个指令的一个示例使用将是一个提供私人和公共条目的博客站点。公共条目可以缓存在任何共享缓存上。下面的代码使用 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 registry 都能被找到(注意不是所有的都适用于响应)。

如果你想使用头部来完全禁用缓存,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 在请求阶段运行,从头到尾应用中间件,因此列表顶部的条目首先在请求阶段运行。在其他中间件更新 Vary 头部后,FetchFromCacheMiddleware 也需要运行,因此 FetchFromCacheMiddleware 必须在任何条目之后运行。

Back to Top