django与缓存

本文总阅读量

前记

简单的缓存设计与在django使用缓存

1.浏览器的缓存

关于浏览器与服务器缓存机制

1.1浏览器缓存机制

1.1.1 http报文的Cache-Control头域

  Cache-Control指定请求和响应遵循的缓存机制。在请求消息或响应消息中设置Cache-Control并不会修改另一个消息处理过程中的缓存处理过程。请求时的缓存指令包括no-cache、no-store、max-age、max-stale、min-fresh、only-if-cached,响应消息中的指令包括public、private、no-cache、no-store、no-transform、must-revalidate、proxy-revalidate、max-age。各个消息中的指令含义如下:
  Public指示响应可被任何缓存区缓存。
  Private指示对于单个用户的整个或部分响应消息,不能被共享缓存处理。这允许服务器仅仅描述当用户的部分响应消息,此响应消息对于其他用户的请求无效。
  no-cache指示请求或响应消息不能缓存
  no-store用于防止重要的信息被无意的发布。在请求消息中发送将使得请求和响应消息都不使用缓存。
  max-age指示客户机可以接收生存期不大于指定时间(以秒为单位)的响应。
  min-fresh指示客户机可以接收响应时间小于当前时间加上指定时间的响应。
  max-stale指示客户机可以接收超出超时期间的响应消息。如果指定max-stale消息的值,那么客户机可以接收超出超时期指定值之内的响应消息。
综述:
Cache-Control指定请求和响应遵循的缓存机制。在请求消息或响应消息中设置Cache-Control并不会修改另一个消息处理过程中的缓存处理过程。请求时的缓存指令包括no-cache、no-store、max-age、max-stale、min-fresh、only-if-cached,响应消息中的指令包括public、private、no-cache、no-store、no-transform、must-revalidate、proxy-revalidate、max-age。网页的缓存通过HTTP消息头中的“Cache-control”来控制,常见的取值有private、no-cache、max-age、must-revalidate等,默认为private。

1.1.1 http报文的Last-Modified/If-Modified-Since

Last-Modified/If-Modified-Since要配合Cache-Control使用。

Last-Modified:标示这个响应资源的最后修改时间。web服务器在响应请求时,告诉浏览器资源的最后修改时间。
If-Modified-Since:当资源过期时(使用Cache-Control标识的max-age),发现资源具有Last-Modified声明,则再次向web服务器请求时带上头 If-Modified-Since,表示请求时间。web服务器收到请求后发现有头If-Modified-Since 则与被请求资源的最后修改时间进行比对。若最后修改时间较新,说明资源又被改动过,则响应整片资源内容(写在响应消息包体内),HTTP 200;若最后修改时间较旧,说明资源无新修改,则响应HTTP 304 (无需包体,节省浏览),告知浏览器继续使用所保存的cache。

If-None-Match

作用: If-None-Match和ETag一起工作,工作原理是在HTTP Response中添加ETag信息。 当用户再次请求该资源时,将在HTTP Request 中加入If-None-Match信息(ETag的值)。如果服务器验证资源的ETag没有改变(该资源没有更新),将返回一个304状态告诉客户端使用本地缓存文件。否则将返回200状态和新的资源和Etag. 使用这样的机制将提高网站的性能

Pragma

作用: 防止页面被缓存, 在HTTP/1.1版本中,它和Cache-Control:no-cache作用一模一样
Pargma只有一个用法, 例如: Pragma: no-cache

2.Django的缓存

2.1Django缓存原理:

Django根据设置的缓存方式,浏览器第一次请求时,cache会缓存单个变量或整个网页等内容到硬盘或者内存中,同时设置response头部,当浏览器再次发起请求时,附带f-Modified-Since请求时间到Django,Django 发现f-Modified-Since会先去参数之后,会与缓存中的过期时间相比较,如果缓存时间比较新,则会重新请求数据,并缓存起来然后返回response给客户端,如果缓存没有过期,则直接从缓存中提取数据,返回给response给客户端。

2.2通用设置

2.2.1设置中间件

django接受请求时,中间件的访问顺序是从上到下
返回响应时,中间件的访问顺序是从下到上,所以中间件的位置要放对

1
2
3
'django.middleware.cache.UpdateCacheMiddleware',  #必须设置在第一个位置 
...其他中间件...
'django.middleware.cache.FetchFromCacheMiddleware',#必须设置在最后一个位置

缓存中间件缓存每个没有GET或者POST参数的页面。 或者,如果CACHE_MIDDLEWARE_ANONYMOUS_ONLY设置为True,只有匿名请求(即不是由登录的用户)将被缓存。 如果想取消用户相关页面(user-specific pages)的缓存,例如Djangos 的管理界面,这是一种既简单又有效的方法。 CACHE_MIDDLEWARE_ANONYMOUS_ONLY,你应该确保你已经启动AuthenticationMiddleware。

此外,缓存中间件为每个HttpResponse自动设置了几个头部信息:

当一个新(没缓存的)版本的页面被请求时设置Last-Modified头部为当前日期/时间。

设置Expires头部为当前日期/时间加上定义的CACHE_MIDDLEWARE_SECONDS。

设置Cache-Control头部来给页面一个最长的有效期,值来自于CACHE_MIDDLEWARE_SECONDS设置。

2.2.2通用设置

1
2
3
4
CACHE_MIDDLEWARE_ALIAS = 'default'    #用来存储的缓存别名
CACHE_MIDDLEWARE_SECONDS = 0 #所有页面默认缓存时间,默认600
CACHE_MIDDLEWARE_KEY_PREFIX ='www.demo.com' #关键的前缀,当多个站点使用同一个配置的时候,这个可以设置可以避免发生冲突,一般设置为网站域名
CACHE_MIDDLEWARE_ANONYMOUS_ONLY = False #那么只有匿名的请求会被缓存,这是一个禁用缓存非匿名用户页面的最简单的做法,注意确保已经启用了Django用户认证中间件

2.3 选择缓存方式

2.3.1memcached缓存(推荐)

需要pip安装memcached的插件Python-mencached和pylibmc,可以同时支持多个服务器上面的memcached

1
2
3
4
5
6
7
8
9
10
11
12
'django.core.cache.backends.memcached.MemcachedCache'
'django.core.cache.backends.memcached.PyLibMCCache'
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
'LOCATION': [
'172.19.26.240:11211', #服务器1
'172.19.26.242:11211', #服务器2
]
#'LOCATION': 'unix:/tmp/memcached.sock', #unix的socket的方式
}
}

2.3.2数据库缓存(不推荐)

注意的是数据库缓存使用的是你配置文件中的数据库作为默认的数据库,如果一定要选用其他的数据库,则需要修改数据库缓存表,不过量多的话,数据库缓存效用也会变差
生成数据库表:
运行命令:python manage.py createcachetable my_cache_table
配置:

1
2
3
4
5
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.db.DatabaseCache',
'LOCATION': 'my_cache_table',
}

2#### 2.3.3文件系统缓存
注意是绝对位置(从根目录开始),必须保证服务器对你列出的路径具有读写权限

1
2
3
4
5
6
7
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
'LOCATION': '/var/tmp/django_cache',#这个是文件夹的路径
#'LOCATION': 'c:\foo\bar',#windows下的示例
}
}

2.3.4本地内存缓存(生产环境下不建议使用)

本地内存缓存,这个缓存是多进程和线程安全的

1
2
3
4
5
6
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'unique-snowflake'
}
}

缓存LOCATION用来区分每个内存存储,如果只有一个本地内存缓存,可以忽略这个设置;但如果有多个的时候,需要至少给他们中一个赋予名字以区分他们。
注意每个进程都有它们自己的私有缓存实例,所以跨进陈缓存是不可能的,因此,本地内存缓存不是特别有效率的,建议只是在内部开发测时使用,不建议在生产环境中使用

2.3.5虚拟缓存(开发时使用)

虚拟缓存,实际是没有缓存,只不过设置缓存时间,请求的时候一样会设置这个时间,但是服务器并没有缓存起来,这个在开发测试阶段,不希望缓存,发布之后则希望缓存,非常实用,到发布的时候,直接把dummy.DummyCache缓存真真的缓存方式即可,其他的无需修改代码

1
2
3
4
5
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
}
}

2.3.6自定义缓存

配置参数来使用其他缓存 如Redis

1
2
3
4
5
CACHES = {
'default': {
'BACKEND': 'path.to.backend',
}
}

2.4缓存级别(本篇来自于于djang-book)

2.4.1view级别缓存

1
2
3
4
5
6
7
from django.views.decorators.cache import cache_page

def my_view(request):
# ...

my_view = cache_page(my_view, 60 * 15)

也可以使用

1
2
3
@cache_page(60 * 15)
def my_view(request):
# ...

cache_page 只接受一个参数: 以秒计的缓存超时时间。 (注意: 为了提高可读性,该参数被书写为 60 * 15 。 60 * 15 将被计算为 900 ,也就是说15 分钟乘以每分钟 60 秒。)

和站点缓存一样,视图缓存与 URL 无关。 如果多个 URL 指向同一视图,每个视图将会分别缓存。 继续 my_view 范例,如果 URLconf 如下所示:

1
2
3
urlpatterns = ('',
(r'^foo/(\d+)/$', my_view),
)

2.4.2 在 URLconf 中指定视图缓存

假设有不同的url指向同一个view但是只想让foo url缓存,那么可以在 URLconf 中指定视图缓存。

1
2
3
urlpatterns = ('',
(r'^foo/(\d+)/$', my_view),
)

改为

1
2
3
4
5
from django.views.decorators.cache import cache_page

urlpatterns = ('',
(r'^foo/(\d+)/$', cache_page(my_view, 60 * 15)),
)

即可完成

2.4.3模板碎片缓存

你同样可以使用cache标签来缓存模板片段。 在模板的顶端附近加入

1
{% load cache %}

以通知模板存取缓存标签。

模板标签

1
{% cache %}

在给定的时间内缓存了块的内容。 它至少需要两个参数: 缓存超时时间(以秒计)和指定缓存片段的名称。 示例:

1
2
3
4
{% load cache %}
{% cache 500 sidebar %}
.. sidebar ..
{% endcache %}

有时你可能想缓存基于片段的动态内容的多份拷贝。 比如,你想为上一个例子的每个用户分别缓存侧边栏。 这样只需要给

1
{% cache %}

传递额外的参数以标识缓存片段。

1
2
3
4
{% load cache %}
{% cache 500 sidebar request.user.username %}
.. sidebar for logged in user ..
{% endcache %}

传递不止一个参数也是可行的。 简单地把参数传给

1
{% cache %}。

缓存超时时间可以作为模板变量,只要它可以解析为整数值。 例如,如果模板变量my_timeout值为600,那么以下两个例子是等价的。

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

这个特性在避免模板重复方面非常有用。 可以把超时时间保存在变量里,然后在别的地方复用。

2.4.4低层次缓存API

有些时候,对整个经解析的页面进行缓存并不会给你带来太多好处,事实上可能会过犹不及。

比如说,也许你的站点所包含的一个视图依赖几个费时的查询,每隔一段时间结果就会发生变化。 在这种情况下,使用站点级缓存或者视图级缓存策略所提供的整页缓存并不是最理想的,因为你可能不会想对整个结果进行缓存(因为一些数据经常变化),但你仍然会想对很少变化的部分进行缓存。

针对这样的情况,Django提供了简单低级的缓存API。 你可以通过这个API,以任何你需要的粒度来缓存对象。 你可以对所有能够安全进行 pickle 处理的 Python 对象进行缓存: 字符串、字典和模型对象列表等等。 (查阅 Python 文档可以了解到更多关于 pickling 的信息。)

缓存模块django.core.cache拥有一个自动依据CACHE_BACKEND设置创建的django.core.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_BACKEND 设置中的 timeout 参数.

如果缓存中不存在该对象,那么cache.get()会返回None。

#Wait 30 seconds for ‘my_key’ to expire…

cache.get(‘my_key’)
None

我们不建议在缓存中保存 None 常量,因为你将无法区分你保存的 None 变量及由返回值 None 所标识的缓存未命中。

cache.get() 接受一个 缺省 参数。 它指定了当缓存中不存在该对象时所返回的值:

cache.get(‘my_key’, ‘has expired’)
‘has expired’

使用add()方法来新增一个原来没有的键值。 它接受的参数和set()一样,但是并不去尝试更新已经存在的键值。

cache.set(‘add_key’, ‘Initial value’)
cache.add(‘add_key’, ‘New value’)
cache.get(‘add_key’)
‘Initial value’

如果想确定add()是否成功添加了缓存值,你应该测试返回值。 成功返回True,失败返回False。

还有个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.delete() 显式地删除关键字。

cache.delete(‘a’)

也可以使用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),增减操作才是原子的。 然而,如果后端并不原生支持增减操作,也可以通过取值/更新两步操作来实现。

3 BACKEND缓存设置

除了在服务器与浏览器设置缓存外,还可以在BACKEND设置缓存。
比如收藏数量,用户每点击一次收藏时,收藏量就+1.如果每次都写会数据库,不免数据量太大(而且存在单一行数据时,写锁优先于读锁,那么碰巧在写数据时,服务器要先等写完才可以进行读),而且数据库还要求一致性。
而同时,浏览数并不要求保证一致性,只要大概准确就行了。
所以这时候,我们可以先将收藏数写入 redis,满足一定条件后,再回写数据库。每次都是+1,+1。当满足于10条或者100条时,就可以一起写入数据库。

4.1使用示例

模板碎片缓存

在搭建一个项目时,打算使用缓存,会发现网页是酱子的
示例
页面中有3个组件
组件1数据更新量最低,只要新加入的机构是以上城市或是类别外,这个组件的数据才会更新。
组件3的数据更新量第二低,由于都是通过统计点击量或者学习人数在进行排名,可能会半天或者一天才更新一次。
组件3由于有课程更新或者学习变化就要更新,所以更新周期可能在小时级别。
所以我们在设置缓存时不能整页统一缓存,而是要分开缓存。
如果这时候我们是通过一个view放回整个页面的话,就需要使用模板碎片缓存,为每个组件设置不同的缓存时间。

如果每个组件使用ajax控制的话,可以使用view级别的缓存,为每个view设置对应的缓存时间。

被动过期缓存

上面的是使用主动过期缓存,除了主动过期缓存外,还可以使用被动过期缓存
示例
图中是整个html一起加载的(未用ajax)。可以看出章节部分是短时间内改动比较少的,而评论是会经常改动的,如果这个资源不算热门的话(或者发布比较久,现在访问的比较少了),评论的变动周期就会比较长,评论如果是用富文本或者markdowm渲染的话却会花费一些资源,这时候可以使用被动过去缓存。可以考虑在评论的models加上一个统计数量,取出评论时,向BACKEND缓存评论数量和评论的内容,每当有用户访问时,比较BACKEND缓存的评论数量和models的统计数量,如果一致则返回缓存内容,不一致则从Model重新生成数据并返回。

附:http协议的缓存

查看评论