Django添加Last-Modified和ETag


用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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM