Django 视图设置超时资源缓存

问题背景

正在使用 Django,问题是这样的,我在后台存放了一些动态的文件资源。

然后又这么一个视图,例如图片:

# models.py

class FileReference(models.Model):
    """
    文件引用,实际引用文件通过一对一引用到此对象
    用以计算 FileBase 的引用计数
    """
    file = models.ForeignKey(FileBase, related_name='refs', on_delete=models.PROTECT)
    name = models.CharField(max_length=260)
    mime = models.CharField(max_length=100)
    timestamp = models.DateTimeField(auto_now_add=True)

    def response_download(self):
        """
        Send a file through Django without loading the whole file into
        memory at once. The FileWrapper will turn the file object into an
        iterator for chunks of 8KB.
        """
        from django.core.servers.basehttp import FileWrapper
        from django.http import StreamingHttpResponse
        from django.utils.http import urlquote
        filename = self.get_path()  # Select your file here.
        wrapper = FileWrapper(open(filename, 'rb'))
        response = StreamingHttpResponse(wrapper, content_type=self.mime)
        response['Content-Length'] = os.path.getsize(filename)
        response['Content-Disposition'] = 'attachment; filename='+urlquote(self.name)
        return response
# view.py

def attachment(request, pk):
    return FileReference.objects.get(pk=pk).response_download()
# urls.py

urlpatterns = patterns('erp.views.get',

    # sys
    url(r'^attachment/(?P<pk>\d+)/', 'sys.attachment', name='sys_attachment'),

    # ....

)

上面一点都不重要,重要的是,这个 attachment 的视图函数会返回一个 StreamingHttpResponse 的 http 响应流,是从文件系统中读取文件然后搞成二进制流 http 返回去。

在我的应用中,这是一个头像图片,也就是会这样来用:

<img src="{% url 'attachment' pk=user.avatar.pk %}" />

这里就会显示一个图片,如果不加设置,像这种情况的请求,每次都不会缓存。

所以大哥,图片都不缓存是会死人的,怎么破?

解决方案

所以这里要用到一些比较高阶的 django 技巧,因为这个 url 是托管给 django 的 wsgi 处理的,所以 nginx 对其也无能为力。

我们先要从 HTTP 的工作原理来考虑缓存的过程,我们的目标是,第一次获取这个 url 的时候返回响应流,后面的话就返回 304 响应码,不执行视图内部的代码,这样就避免了服务器的运算以及大块头的 http body 的传输,效率提高很多!


然后的话,浏览器端的缓存是怎么实现的呢?

首先,对于所有可以缓存的 url 请求下来的资源,浏览器会记录三个信息:

  1. 请求的资源的 url;
  2. 上次资源的时间戳(这个资源最近什么时候改动过);
  3. 上次请求的 http body,也就是缓存的内容。

注释一下,“可以缓存”的条件是,服务器返回的 http 响应里面包含 Last Modified 时间戳的头信息:

# HTTP Response Header
Last-Modified:Mon, 01 Jan 1990 00:00:00 GMT

那么,如果下一次浏览器需要请求同一个 url 的时候,浏览器就会把上次请求资源的时间戳在 http request header 里面发送出去,格式大概是这样子:

# HTTP Request Header
If-Modified-Since:Mon, 01 Jan 1990 00:00:00 GMT

然后,http 服务接收到这个请求的时候,服务器端总是可以通过某种逻辑决定是否返回 304 Not Modified 状态码,并且返回空的响应体。

这个“某种逻辑”对于 If-Modified-Since 这种情况是这样的:

服务器检查请求的资源(其实甚至可以是一篇博客文章)在服务器端最新的修改时间,如果这个时间等于或早于 If-Modified-Since 这个时间,说明服务器端没有更新内容,这种情况下就返回 304。


了解这种逻辑之后,我们看回 django:

django 可以在视图里面方便地返回一个时间戳,来告诉浏览器这个视图的更新时间(这可以动态指定)

那么浏览器第一次获取这个视图的时候就会返回这个 Last-Modified 时间戳。

然后下次再请求同一个视图的时候,这个视图就会先计算 Last-Modified 的时间戳,然后比较传过来的 If-Modified-Since,如果前者小于等于后者,那么就返回 304。


这个动态指定 Last-Modified 的方式,Django 提供了一个视图的 decorator:

参考文档:https://docs.djangoproject.com/en/1.7/topics/conditional-view-processing/

具体的调用方法还是请自己读透文档,我这里只说这个场景(第一次获取这个 url 的时候返回响应流,后面的话就返回 304 响应码)下的应用:

# view.py

from django.views.decorators.http import last_modified


def always_cache(request, *args, **kwargs):
    """ 这个函数总是返回一个早的日期,供 @last_modified 调用以确保总是缓存 """
    return datetime(1990, 1, 1, 0, 0)


@last_modified(always_cache)
@login_required
def attachment(request, pk):
    return FileReference.objects.get(pk=pk).response_download()

可以看到我们调用了一个 last_modified 的 decorator,这个 decorator 调用了一个函数 always_cache,这个 always_cache 总是返回一个固定的时间。

意思就是说,这个资源永远都是在同一个时刻被标记的,无论再次如何请求,它都是“旧的”,于是会返回 304 而调取缓存。

试验了一下,刷新页面之后,这个图片的请求响应返回 304,实验成功!

这里特意需要提醒一下,我这里用了两个视图修饰器,另一个是 @login_required,这时候要注意 @last_modified 务必放在最外层(第一个),因为它是会直接返回 HTTPResponse 对象的,因此不可以再链式处理。

后记

通过这个例子要更深刻了解 HTTP 的缓存机制,对于可按时间缓存的资源,这仅仅取决于 HTTP 的响应头有没有指定 Last-Modified 时间戳,而不管它的 url 是任何的形式。

简化写法

事实上这个 always_cache 其实用一个 lambda 表达式就搞定了,可以把代码进一步压缩:

@last_modified(lambda request, pk: datetime(1990, 1, 1, 0, 0))
@login_required
def attachment(request, pk):
    return FileReference.objects.get(pk=pk).response_download()

【转载请附】愿以此功德,回向 >>

原文链接:https://www.huangwenchao.com.cn/2015/02/django-view-cache-304.html【Django 视图设置超时资源缓存】

发表评论

电子邮件地址不会被公开。 必填项已用*标注