Django 缓存框架

作者:Django 团队
译者:weizhong2004@gmail.com
翻译开始日期:2006-05-08
翻译完成日期:2006-05-08
最后修订日期:2006-05-08
原文版本:2758

动态网站的基本功能就在于, 没错, 它是动态的. 用户每次请求一个页面时, Web 服务器都要进行全面的计算 -- 从数据库查询到渲染业务逻辑 -- 直到生成最终展示的页面. 从服务器负载的角度来看,这远比仅仅从文件系统读取一个文件展示要占用的系统资源多得多.

对绝大多数网站应用程序来说, 这点负载不是大问题.绝大部分网站应用不是 washingtonpost.com 或 slashdot.org 这样繁忙; 它们通常是小规模-中等规模的,没有特别高的点击率. 但对大规模高点击率的站点来说, 就应该尽可能的降低WEB服务器的负载.

这正是需要缓存机制的原因.

建立缓存机制的目的就是在适当的时候省略掉计算步骤(再次访问时).下面用伪码来阐述该机制如何运作:

根据 URL 请求, 看看缓存中有没有该页面
如果缓存中有该页面:
    返回该页面
否则:
    生成该页面
    将该页面保存在缓存中(以备下次访问)
    返回这具生成的页面

Django 自带一个高效实用的缓存系统, 允许你保存动态页面, 这样就不必在每次请求时都通过计算生成新页面.为了使用上的方便, Django提供了不同层次的缓存模式: 你可以仅缓存指定 views 的输出, 也可以只缓存高计算量的的片段, 只要你愿意,你也可以缓存你的整个站点.

Django 与 "upstream" 缓存可以很好的协作, 就象 Squid(http://www.squid-cache.org/) 及 browser-based 缓存一样. 这些缓存方式, 你不能直接控制但可以通过 HTTP headers 来告知站点的哪些部分需要缓存及如何缓存.

设置缓存

缓存系统需要进行不太复杂的设置才可以工作. 也就是说你必须告诉它你的缓存数据要放在哪儿 -- 或者是数据库, 或者是文件系统, 或者干脆就放在内存里.这个选择对缓存的性能有着非常重大的影响.没错,某些缓存类型远比其它的快!

通过设置你的 settings 文件的 CACHE_BACKEND 设置来决定 cache 类型. 下面详细介绍该选项的所有可选值:

Memcached

这是 Django 缓存系统中最有效率的方式, Memcached 是完全的基于内存的缓存框架, 最初开发它来处理 LiveJournal.com 的高流量后来被 Danga Interactive 公司开源. 它通常用于象 Slashdot 及 Wikipedia 这样的站点以减少数据库检索, 它极大的提高了站点的性能.

Memcached 可以通过 http://danga.com/memcached/ 免费得到. 它以一个后台监视程序的方式运作, 使用分配给它的指定容量的内存. 它所做的就是提供一个接口 -- 一个 super-lightning-fast 接口 -- 添加,得到及删除缓存数据的接口.所有数据直接保存在内存中,不占用任何文件系统及数据库资源.

在安装了 Memcached 之后,你需要安装 Memcached 的 Python 绑定. 它是一个独立的 Python 模块, memcache.py, 可以通过ftp://ftp.tummy.com/pub/python-memcached/ 得到. 如果该 URL 无法访问, 就直接到 Memcached 的 Web 站点 (http://www.danga.com/memcached/), 然后通过 "Client APIs" 部分得到它的 Python 绑定.

要在 Django 中使用 Memcached , 设置 CACHE_BACKENDmemcached://ip:port/, 这里的 ip 是 Memcached 守护程序的 IP 地址, 而 port 则是 Memcached 进程使用的端口.

在下面这个例子里, Memcached 运行在 localhost (127.0.0.1) 的 11211 端口:

CACHE_BACKEND = 'memcached://127.0.0.1:11211/'

Memcached 的一个出色特性是它在多个服务器间共享缓存的能力. 要使用这个高级特性, 在 CACHE_BACKEND 中包含多个服务器的IP地址,地址间用分号分隔. 下面这个例子共享运行在 172.19.26.240 及 172.19.26.242 的 Memcached 实例, 这两个实例均使用 11211 端口:

CACHE_BACKEND = 'memcached://172.19.26.240:11211;172.19.26.242:11211/'

基于内存的缓存机制有一个缺点: 由于缓存数据保存在内存中, 当服务器崩溃时缓数据将会丢失.谁都知道内存不适合用来保存永久数据, 所以不应该依赖基于内存的缓存来作为你唯一的数据保存方式.实际上, 没有一种 Django 缓存后端适合保存永久数据 -- 他们之所以存在就是为了缓存这一个目的, 而不是永远储存数据 -- 我们在这里指出这一点是因为基于内存的缓存它比另外的方式更加临时化.

数据库缓存

要使用数据库来做为你的缓存后端, 首先要在你的数据库中建立一个缓存表. 运行下面的命令:

python manage.py createcachetable [cache_table_name]

...这里的 [cache_table_name] 是数据库中要创建的缓存表的名字.(你可以取任意的名字,只要不和已有的表重名就行).这个命令创建在数据库中创建一个适当结构的缓存表, Django 随后使用它来缓存你的数据.

在完成缓存表的创建之后, 设置 CACHE_BACKEND"db://tablename/", 这里的 tablename 是缓存表的名字.下面这个例子里,缓存表的名是 my_cache_table:

CACHE_BACKEND = 'db://my_cache_table'

只要你有一个快速的,索引良好的数据库服务器, 数据库缓存就会极佳的运转.

文件系统缓存

要在文件系统中保存缓存数据, 在 CACHE_BACKEND 中使用 "file://" 缓存类型. 下面这个例子将缓存数据保存在 /var/tmp/django_cache 中, 设置如下:

CACHE_BACKEND = 'file:///var/tmp/django_cache'

注意 file: 之后是三个斜线而不是两个. 前两个是 file:// 协议, 第三个是路径 /var/tmp/django_cache 的第一个字符.

这个目录路径必须是绝对路径 -- 也就是说,它应该从你的文件系统的根开始算起.至于路径的最后是否加一个斜线, Django 并不在意.

要确保这个设定的路径存在并且 Web 服务器可以读写.继续上面的例子,如果你的服务器以 apache 用户身份运行, 确保目录 /var/tmp/django_cache 存在并且可以被用户 apache 读写.

本地内存缓存

如果你想要内存缓存的高性能却没有条件运行 Memcached, 可以考虚使用本地内存缓存后端. 这个缓存后端是多进程的并且线程安全. 要使用它,设置 CACHE_BACKEND"locmem:///". 举例来说:

CACHE_BACKEND = 'locmem:///'

简单缓存(用于开发)

"simple:///" 是一个简单的,单进程的内存缓存类型. 它仅仅在进程中保存缓存数据, 这意味着它仅适用于开发或测试环境. 例子:

CACHE_BACKEND = 'simple:///'

虚拟缓存 (用于开发)

最后, Django还支持一种 "dummy" 缓存(事实上并未缓存) -- 仅仅实现了缓存接口但未实际做任何事.

This is useful if you have a production site that uses heavy-duty caching in various places but a development/test environment on which you don't want to cache. In that case, set CACHE_BACKEND to "dummy:///" in the settings file for your development environment. As a result, your development environment won't use caching and your production environment still will.

CACHE_BACKEND 参数

所有缓存类型均接受参数. 提供参数的方式类似查询字符串风格. 下面列出了所有合法的参数:

timeout
默认的缓存有效时间,以秒计. 默认值是 300 秒(五分钟).
max_entries
用于 简单缓存数据库缓存 后端, 缓存的最大条目数(超出该数旧的缓存会被清除,默认值是 300).
cull_percentage

当达到缓存的最大条目数时要保留的精选条目比率. 实际被保存的是 1/cull_percentage, 因此设置 cull_percentage=3 就会保存精选的 1/3 条目上,其余的条目则被删除.

如果将 cull_percentage 设置为 0 则意味着当达到缓存的最大条目数时整个缓存都被清除.当缓存命中率很低时这会 极大的 提高精选缓存条目的效率(根本不精选).

这个例子里, timeout 被设置为 60:

CACHE_BACKEND = "memcached://127.0.0.1:11211/?timeout=60"

这个例子, timeout 设置为 30max_entries 设置为 400:

CACHE_BACKEND = "memcached://127.0.0.1:11211/?timeout=30&max_entries=400"

非法的参数会被忽略.

缓存整个站点

设置了缓存类型之后, 最简单使用缓存的方式就是缓存整个站点. 在``MIDDLEWARE_CLASSES`` 设置中添加 django.middleware.cache.CacheMiddleware , 就象下面的例子一样:

MIDDLEWARE_CLASSES = (
    "django.middleware.cache.CacheMiddleware",
    "django.middleware.common.CommonMiddleware",
)

( MIDDLEWARE_CLASSES 顺序相关. 参阅下文中的 "Order of MIDDLEWARE_CLASSES")

然后,在 Django 设置文件中添加以下设置:

缓存中间件缓存没有 GET/POST 参数的每个页面.另外, CacheMiddleware 自动在每个 HttpResponse 中设置一些 headers:

参阅 middleware documentation 了解中间件的更多信息.

缓存单个 view

Django 能够只缓存特定的页面. django.views.decorators.cache 定义了一个 cache_page 修饰符, 它能自动缓存该 view 的响应. 该修饰符的使用极为简单:

from django.views.decorators.cache import cache_page

def slashdot_this(request):
    ...

slashdot_this = cache_page(slashdot_this, 60 * 15)

或者, 使用Python 2.4 的修饰符语法:

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

cache_page 仅接受一个参数: 缓存有效期,以秒计. 在上面的例子里, slashdot_this() view 将被缓存 15 分钟.

底层缓存 API

某些时候, 缓存一个完整的页面不符合你的要求. 比如你认为仅有某些高强度的查询才有必要缓存其结果.要达到这种目的,你能使用底层缓存 API 来在任意层次保存对象到缓存系统.

缓存 API 是简单的. 从缓存模块 django.core.cache 导出一个由 CACHE_BACKEND 设置自动生成的 cache 对象:

>>> from django.core.cache import cache

基本的接口是 set(key, value, timeout_seconds)get(key):

>>> cache.set('my_key', 'hello, world!', 30)
>>> cache.get('my_key')
'hello, world!'

timeout_seconds 参数是可选的,其默认值等于 CACHE_BACKENDtimeout 参数.

如果缓存中没有该对象, cache.get() 返回 None:

>>> cache.get('some_other_key')
None

# Wait 30 seconds for 'my_key' to expire...

>>> cache.get('my_key')
None

get() 可以接受一个 default 参数:

>>> cache.get('my_key', 'has expired')
'has expired'

当然还有一个 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}

最后, 你可以使用 delete() 显式的删除键. 这是一个在缓存中清除特定对象的简便的方式:

>>> cache.delete('a')

就是这样. 缓存机制限制非常少: 你可以安全的缓存能被 pickled 的任意对象(key必须是字符串).

Upstream caches

到现在为止, 我们的目光仅仅聚焦在 你自己的 数据上. 在 WEB 开发中还有另外一种类型的缓存: "upstream" 缓存. 这是用户请求还未抵达你的站点时由浏览器实施的缓存.

下面是 upstream 缓存的几个例子:

  • 你的 ISP 会缓存特定页面, 当你请求 somedomain.com 的一个页面时, 你的 ISP 会直接发送给你一个缓存页.(不访问 somedomain.com ).
  • 你的 Django Web 站点可能建立在一个 Squid (http://www.squid-cache.org/) Web 代理服务器之后, 它会缓存页面以提高性能. 这种情况下,每个请求会先经 Squid 处理, 仅在需要时它会将请求传递给你的应用程序.
  • 你的浏览器也会缓存一些页面. 如果一个 WEB 页发送了正确的 headers, 浏览器会用本地(缓存的)拷贝来回应后发的同一页面的请求.

Upstream 缓存是一个非常有效的推进, 不过它也有相当不足之处: 很多 WEB 页基于授权及一堆变量, 而这个缓存系统盲目的单纯依赖 URL 缓存页面, 这可能会对不适当的用户泄露敏感信息.

举例来说,假设你使用一个 Web e-mail 系统, "inbox" 页的内容显然依赖当前登录用户. 如果一个 ISP 盲目的缓存了你的站点, 后来的用户就会看到前一用户的收件箱, 这可不是一件有趣的事.

幸运的是, HTTP 提供了一个该问题的解决方案: 用一系列 HTTP headers 来构建缓存机制以区分缓存内容, 这样缓存系统就不会缓存某些特定页.

使用 Vary headers

其中一个 header 就是 Vary. 它定义了缓存机制在创建缓存 key 时的请求 headers. 举例来说, 如果一个网页的内容依赖一个户的语言设置, 则该网页被告知 "vary on language."

默认情况, Django 的缓存系统使用请求路径创建 缓存 key -- 比如, "/stories/2005/jun/23/bank_robbed/". 这意味着该 URL 的每个请求使用相同的缓存版本, 不考虑用户代理的不同.(cookies 及语言特性).

因此我们需要 Vary .

如果你的基于 Django 的页面根据不同的请求 headers 输出不同的内容 -- 比如一个 cookie, 或语言, 或用户代理 -- 你会需要使用 Vary header 来告诉缓存系统这个页面输出依赖这些东西.

要在 Django 中做到这一步, 使用 vary_on_headers view 修饰符,就象下面这样:

from django.views.decorators.vary import vary_on_headers

# Python 2.3 syntax.
def my_view(request):
    ...
my_view = vary_on_headers(my_view, 'User-Agent')

# Python 2.4 decorator syntax.
@vary_on_headers('User-Agent')
def my_view(request):
    ...

这样缓存系统 (比如 Django 自己的缓存中间件) 会为不同的用户代理缓存不同的版本的页面.

使用 vary_on_headers 修饰符的优势在于(与人工设置 Vary header 相比:使用类似 response['Vary'] = 'user-agent')修饰符会添加到 Vary header (可能已存在) 而不是覆盖掉它.

你也可以传递多个 header 给 vary_on_headers():

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

由于多个 cookie 的情况相当常见, 这里有一个 vary_on_cookie 修饰符. 下面两个 views 是等价的:

@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:

from django.utils.cache import patch_vary_headers
def my_view(request):
    ...
    response = render_to_response('template_name', context)
    patch_vary_headers(response, ['Cookie'])
    return response

patch_vary_headers 接受一个 HttpResponse 实例作为它的第一个参数及一个 header 名字的列表或tuple作为第二个参数.

要了解 Vary headers 的更多信息, 参阅 official Vary spec.

控制缓存: 使用其它 headers

缓存的另一个问题是数据的私密性及瀑布缓存模式下数据保存到哪里.

用户经常要面对的有两种缓存: 他自己的浏览器缓存(私人缓存) 及站点提供的缓存(公开缓存).一个公开缓存用于多用户情况, 其内容由另外的人控制. 这造成了数据的私密性问题: 你当然不想你的银行帐号保存在公共缓存里. 因此应用程序需要一种方式告诉缓存系统哪些东西是私密的,哪些则是公开的.

解决方案就是声明某个页面的缓存是 "私密的". 在 Django 中, 使用 cache_control view 修饰符. 例子:

from django.views.decorators.cache import cache_control
@cache_control(private=True)
def my_view(request):
    ...

这个修饰符会在幕后谨慎的发送适当的 HTTP header 发避免上面的问题.

还有一些其它的方式控制缓存参数. 举例来说,HTTP 允许应用程序做以下事:

  • 定义一个页面被缓存的最大时间.
  • 指定缓存是否需要总是检查新版本, 如果没有变化则仅传送缓存版本. (某些缓存即使服务器端页面变化也仅传递缓存版本--仅仅因为缓存拷贝尚未到期).

在 Django 中, 使用 cache_control view 修饰符指定缓存参数.在这个例子里 cache_control 通知缓存每次检验缓存版本, 直到 3600 秒到期:

from django.views.decorators.cache import cache_control
@cache_control(must_revalidate=True, max_age=3600)
def my_view(request):
    ...

所有合法的 Cache-Control HTTP 指令在 cache_control() 中都是合法的. 下面是完整的指令列表:

  • public=True
  • private=True
  • no_cache=True
  • no_transform=True
  • must_revalidate=True
  • proxy_revalidate=True
  • max_age=num_seconds
  • s_maxage=num_seconds

要了解 Cache-Control HTTP 指令的细节,参阅 Cache-Control spec.

(注意缓存中间件已经通过设置中的 CACHE_MIDDLEWARE_SETTINGS 的值设定了缓存 header 的 max-age. 如果你在 cache_control 修饰符中使用了自定义的 max_age , 修饰符中的设置将被优先使用, header 值会被正确的合并)

其它优化

Django 自带了一些中间件以帮助你提高站点性能:

  • django.middleware.http.ConditionalGetMiddleware 添加了有条件GET的支持. 它利用了 ETagLast-Modified headers.
  • django.middleware.gzip.GZipMiddleware 为支持 Gzip 的浏览器对发送内容进行压缩(所有流行浏览器均支持).

MIDDLEWARE_CLASSES 顺序

如果你使用了 CacheMiddleware, 在 MIDDLEWARE_CLASSES 设置中使用正确顺序非常重要. 由于缓存中间件需要知道哪些 headers 由哪些缓存存储.中间件总是在可能的情况下添加某些东西到 Vary 响应 header.

CacheMiddleware 放到其它可能添加某些东西到 Vary Header的中间件之后,下面的中间件会添加东西到 Vary header:

  • SessionMiddleware 添加了 Cookie
  • GZipMiddleware 添加了 Accept-Encoding