用Django REST Framework做的REST API,其中有个API有这样的需求:

APP端请求这个API,服务器端从数据库读数据,返回json。返回的数据量稍微有些大,但是可能一年才修改一次,所以希望能够仅在数据修改的时候才传输数据,让APP端更新。

1. Last-Modified和ETag

HTTP响应头Last-Modified和ETag都能实现这个需求,关于二者的详细解释,这篇文章说的简单明了:http://www.iwms.net/n2029c12.aspx

原文摘抄如下:

1) 什么是”Last-Modified”?
  在浏览器第一次请求某一个URL时,服务器端的返回状态会是200,内容是你请求的资源,同时有一个Last-Modified的属性标记此文件在服务期端最后被修改的时间,格式类似这样:

  Last-Modified: Fri, 12 May 2006 18:53:33 GMT
  客户端第二次请求此URL时,根据 HTTP 协议的规定,浏览器会向服务器传送 If-Modified-Since 报头,询问该时间之后文件是否有被修改过:

  If-Modified-Since: Fri, 12 May 2006 18:53:33 GMT
  如果服务器端的资源没有变化,则自动返回 HTTP 304 (Not Changed.)状态码,内容为空,这样就节省了传输数据量。当服务器端代码发生改变或者重启服务器时,则重新发出资源,返回和第一次请求时类似。从而保证不向客户端重复发出资源,也保证当服务器有变化时,客户端能够得到最新的资源。
2) 什么是”Etag”?
  HTTP 协议规格说明定义ETag为“被请求变量的实体值” (参见 —— 章节 14.19)。 另一种说法是,ETag是一个可以与Web资源关联的记号(token)。典型的Web资源可以一个Web页,但也可能是JSON或XML文档。服务器单独负责判断记号是什么及其含义,并在HTTP响应头中将其传送到客户端,以下是服务器端返回的格式:
  ETag: "50b1c1d4f775c61:df3"
  客户端的查询更新格式是这样的:
  If-None-Match: W/"50b1c1d4f775c61:df3"
  如果ETag没改变,则返回状态304然后不返回,这也和Last-Modified一样。本人测试Etag主要在断点下载时比较有用。
3) Last-Modified和Etags如何帮助提高性能?
  聪明的开发者会把Last-Modified 和ETags请求的http报头一起使用,这样可利用客户端(例如浏览器)的缓存。因为服务器首先产生 Last-Modified/Etag标记,服务器可在稍后使用它来判断页面是否已经被修改。本质上,客户端通过将该记号传回服务器要求服务器验证其(客户端)缓存。
  过程如下:

  1. 客户端请求一个页面(A)。
  2. 服务器返回页面A,并在给A加上一个Last-Modified/ETag。
  3. 客户端展现该页面,并将页面连同Last-Modified/ETag一起缓存。
  4. 客户再次请求页面A,并将上次请求时服务器返回的Last-Modified/ETag一起传递给服务器。
  5. 服务器检查该Last-Modified或ETag,并判断出该页面自上次客户端请求之后还未被修改,直接返回响应304和一个空的响应体。

2. Django中添加Last-Modified和ETag

Django中有三个decorator,可以为响应添加Last-Modified和ETag,参看:https://docs.djangoproject.com/en/1.8/topics/conditional-view-processing/

  • condition(etag_func=None, last_modified_func=None)
  • etag(etag_func)
  • last_modified(last_modified_func)

要注意的是,官网给的使用方法都是针对function-based view,如果你使用的是class-based view,需要在url配置中调用decorator,例如:

url(r'^rest/colours/$',
etag(get_colour_checksum)(views.ColourList.as_view()),
name='colour-list'),

要实现前面的需求的话,思路很简单,获得数据表上次修改的时间或者给数据表算个hash值就行了。

一开始搜到了一个Mysql获取table最近更新时间的方法:http://stackoverflow.com/questions/307438/how-can-i-tell-when-a-mysql-table-was-last-updated

SELECT UPDATE_TIME
FROM information_schema.tables
WHERE TABLE_SCHEMA = 'dbname'
AND TABLE_NAME = 'tabname'

在自己的数据库上试了一下,返回却是NULL。仔细一看评论,这个方法在MyISAM上有用,但是在InnoDB上没有用。只好再找其他办法。

后来又找到一个能查看Table Checksum的语句:http://dev.mysql.com/doc/refman/5.5/en/checksum-table.html

CHECKSUM TABLE tbl_name [, tbl_name] ... [ QUICK | EXTENDED ]

测试有用!于是就用这个语句作为了生成ETag的方法。

3. 通过cache framework添加Last-Modified和ETag

因为我使用了Django的cache framework(https://docs.djangoproject.com/en/1.8/topics/cache/),将该请求的缓存时间设置为5分钟。所以有三个响应头部按照我设置的5分钟缓存时间被自动添加:

但是5分钟之后,这个缓存会被刷新,Last-Modified的时间也会相应被刷新,这样无法满足我前面说到的需求。那么应该怎么加入ETag呢?

通过阅读UpdateCacheMiddleware源码,我发现它在处理response的时候调用了函数 patch_response_headers,该函数源码如下:

def patch_response_headers(response, cache_timeout=None):
"""
Adds some useful headers to the given HttpResponse object:
ETag, Last-Modified, Expires and Cache-Control Each header is only added if it isn't already set. cache_timeout is in seconds. The CACHE_MIDDLEWARE_SECONDS setting is used
by default.
"""
if cache_timeout is None:
cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS
if cache_timeout < 0:
cache_timeout = 0 # Can't have max-age negative
if settings.USE_ETAGS and not response.has_header('ETag'):
if hasattr(response, 'render') and callable(response.render):
response.add_post_render_callback(_set_response_etag)
else:
response = _set_response_etag(response)
if not response.has_header('Last-Modified'):
response['Last-Modified'] = http_date()
if not response.has_header('Expires'):
response['Expires'] = http_date(time.time() + cache_timeout)
patch_cache_control(response, max_age=cache_timeout)

从代码中可以看出:

  • 如果Last-Modified, Expires之前未被设置,则这里按照cache的设置为他们赋值。
  • 如果 settings.USE_ETAGS 是True,且ETag未被设置,则会将调用默认方法或自定义方法,给ETag赋值。

因为默认的方法 _set_response_etag 是用响应内容算出MD5值赋给ETag,只要数据未变,ETag值是不变的,可以满足需求。

所以需要做的就是在settings里面加上USE_ETAG=True,就行了。

4. nginx对ETag的支持

通过前面的设置,我在开发机器上已经实现了ETag的添加,可以当我将代码部署到生产环境再测试的时候,确发现ETag没有了。生产环境用的是gunicorn+nginx,所以很自然就想到是nginx那边出了问题。

网上大部分搜到的nginx ETag相关的文章都在说怎么为静态文件添加ETag,和我的情况不一样。

花了很久,最后找到了原因:nginx 1.7.3 之前的版本对ETag支持不好,开了gzip就会自动把ETag删掉。参看:http://stackoverflow.com/questions/31125888/nginx-missing-etag-when-gzip-is-used

解决方法很简单,升级nginx到最新版本就行,连配置都不用修改。升级方法参看:http://nginx.org/en/linux_packages.html

Django添加Last-Modified和ETag的更多相关文章

  1. django 添加comments app

    django 添加comments app 参看 django comments 文档 安装和配置comments 1.安装comments,运行:pip install django-contrib ...

  2. django 添加动态表格的方法

    传统方法(基于方法的视图):http://stellarchariot.com/blog/2011/02/dynamically-add-form-to-formset-using-javascrip ...

  3. django添加静态文件

    最近做了一个todolist webapp,需要稍微添加css时候又忘记django的添加方法了,查看了以前的项目才想起来,所以记录一下. 1.settings.py 将以下代码放到最下面 STATI ...

  4. Django添加ckeditor富文本编辑器

    源码 https://github.com/django-ckeditor/django-ckeditor 通过pip安装. pip3 install django-ckeditor pip3 ins ...

  5. django添加装饰器

    引入模块: from django.utils.decorators import method_decorator 添加:@method_decorator(func) from django.ut ...

  6. Django 添加应用

    一个项目可以添加多个应用,可以使用以下两种方法来添加应用: [root@localhost web]$ python manage.py startapp blog [root@localhost w ...

  7. Django添加tinyMCE编辑器

    tinymce的使用方法很简单,只需要在html页面中包含如下: <!-- Place inside the <head> of your HTML --> <scrip ...

  8. Python - Django - 添加首页尾页上一页下一页

    添加首页和尾页: views.py: from django.shortcuts import render from app01 import models def book_list(reques ...

  9. Python - Django - 添加作者

    在 book_list.html 的页面下方加上 “添加作者” 的链接 <!DOCTYPE html> <html lang="en"> <head& ...

随机推荐

  1. iphone,pad等真机不被xcode识别,解决方法

    问题现象:xcode 的项目在进行真机调试时,xcode不能识别真机, 调测情况分析: 1 设备正常,itunes 能够识别 2 在apple developer 中创建项目的Provisioning ...

  2. Effective Java 62 Document all exceptions thrown by each method

    Principle Always declare checked exceptions individually, and document precisely the conditions unde ...

  3. nginx设置反向代理后,页面上的js css文件无法加载

    问题现象: nginx配置反向代理后,网页可以正常访问,但是页面上的js css文件无法加载,页面样式乱了. (1)nginx配置如下: (2)域名访问:js css文件无法加载: (3)IP访问:j ...

  4. SQL2012数据库加密方法

    1.非对称密钥来保护新的对称密钥 /*--测试环境 Microsoft SQL Server 2012 (SP1) - 11.0.3000.0 (X64) Oct 19 2012 13:38:57 C ...

  5. cocos2d-x之使用plist文件初试

    bool HelloWorld::init() { if ( !Layer::init() ) { return false; } FileUtils *fu=FileUtils::getInstan ...

  6. A python tool to static sim.log duration time

    When working ALU IMS Patch team, we need to static the SU duration to add it to the patch report, th ...

  7. HTTP详解2-请求、响应、缓存

    1. HTTP请求格式 做过Socket编程的人都知道,当我们设计一个通信协议时,“消息头/消息体”的分割方式是很常用的,消息头告诉对方这个消息是干什么的,消息体告诉对方怎么干.HTTP协议传输的消息 ...

  8. DW Basic Knowledge1

    以下内容,常读常新,每次都有新的感悟和认识. 数据仓库必须使组织机构的信息变得容易存取. 数据仓库的内容需要是容易理解的,数据对业务人员也必定是直观的,明显的. 数据仓库重新组织了原来OLTP数据库的 ...

  9. docker containerd中的容器操作

    containerd的中的各种操作都是通过Task来进行的,因此对于容器的create, start, delete等等操作其实都是一个个的Task而已. Task的数据结构如下所示: type Ta ...

  10. AOJ 739 First Blood

    First Blood Time Limit: 1000 ms   Memory Limit: 64 MBTotal Submission: 152   Submission Accepted: 37 ...