http協(xié)議有一系列的緩存機(jī)制(RFC2616),相關(guān)的參數(shù)就在協(xié)議header中。緩存機(jī)制的合理使用可以大大減緩對服務(wù)器的壓力。
1 HTTP緩存頭的設(shè)置參數(shù)
HTTP緩存頭的參數(shù)包括:
- Cache-Control(用于本地緩存)
- Expires(用于本地緩存)
- Last-Modified(協(xié)商緩存)
- Etag(協(xié)商緩存)。
1.1 Cache-Control
指定請求和響應(yīng)遵循的緩存機(jī)制。在請求消息或響應(yīng)消息中設(shè)置Cache- Control并不會修改另一個消息處理過程中的緩 存處理過程。
- 請求時的緩存指令包括no-cache、no-store、max-age、max-stale、min-fresh、only-if- cached;
- 響應(yīng)消息中的指令包括public、private、no-cache、no-store、no-transform、must- revalidate、proxy-revalidate、max-age。
1.2Expires
是一個絕對時間,作用與cache-control的max-age相類似,表示資源信息失效的時間。
1.3 Last-Modified
被訪問的資源的最近一次更改時間(http1.0)
1.4 ETag
資源的一個唯一標(biāo)志(http1.1),通過這個標(biāo)識,可以實現(xiàn)客戶端與服務(wù)端的協(xié)商機(jī)制。它的作用與Last-Modified是相類似的。
2 原理
是事實上,上述四者的實現(xiàn)機(jī)制上,可以歸納為:
- cache-control/Expires:資源有效時間定義機(jī)制
- Last-Modified/ETag:資源更新傳輸協(xié)商機(jī)制
2.1 cache-control/Expires:資源有效時間定義機(jī)制
最好的請求是不必與服務(wù)器進(jìn)行通信的請求:通過響應(yīng)的本地副本,我們可以避免所有的網(wǎng)絡(luò)延遲以及數(shù)據(jù)傳輸?shù)臄?shù)據(jù)成本。為此,HTTP 規(guī)范允許服務(wù)器返回 一系列不同的 Cache-Control 指令,控制瀏覽器或者其他中繼緩存如何緩存某個響應(yīng)以及緩存多長時間。

一些參數(shù)的說明
- no-cache和 no-store
no-cache表示必須先與服務(wù)器確認(rèn)返回的響應(yīng)是否被更改,然后才能使用該響應(yīng)來滿足后續(xù)對同一個網(wǎng)址的請求。因此,如果存在合適的驗證令牌 (ETag),no-cache 會發(fā)起往返通信來驗證緩存的響應(yīng),如果資源未被更改,可以避免下載。
相比之下,no-store更加簡單,直接禁止瀏覽器和所有中繼緩存存儲返回的任何版本的響應(yīng) - 例如:一個包含個人隱私數(shù)據(jù)或銀行數(shù)據(jù)的響應(yīng)。每次用戶請求該資源時,都會向服務(wù)器發(fā)送一個請求,每次都會下載完整的響應(yīng)。 - public和private
如果響應(yīng)被標(biāo)記為public,即使有關(guān)聯(lián)的 HTTP 認(rèn)證,甚至響應(yīng)狀態(tài)碼無法正常緩存,響應(yīng)也可以被緩存。大多數(shù)情況下,public不是必須的,因為明確的緩存信息(例如max-age)已表示 響應(yīng)可以被緩存。
相比之下,瀏覽器可以緩存private響應(yīng),但是通常只為單個用戶緩存,因此,不允許任何中繼緩存對其進(jìn)行緩存 - 例如,用戶瀏覽器可以緩存包含用戶私人信息的 HTML 網(wǎng)頁,但是 CDN 不能緩存。 - max-age
該指令指定從當(dāng)前請求開始,允許獲取的響應(yīng)被重用的最長時間(單位為秒) - 例如:max-age=60表示響應(yīng)可以再緩存和重用 60 秒。 - 關(guān)于expires
Cache-Control 頭在 HTTP/1.1 規(guī)范中定義,取代了之前用來定義響應(yīng)緩存策略的頭(例如 Expires)。當(dāng)前的所有瀏覽器都支持 Cache-Control,因此,使用它就夠了。 - cache-control在請求端和服務(wù)端的命令相互獨(dú)立
cache-control不會因為請求設(shè)置的值而修改響應(yīng)設(shè)置,反之亦然。這里考慮到一種場景,響應(yīng)頭中設(shè)置了一定的緩存時間,然而請求端仍然需要獲取最新結(jié)果,則將請求頭的緩存設(shè)置中加上“max-age=0”,則強(qiáng)制服務(wù)端響應(yīng)這個請求。
2.2 Last-Modified/ETag:資源更新傳輸協(xié)商機(jī)制
Last-Modified
在瀏覽器第一次請求某一個URL時,服務(wù)器端的返回狀態(tài)會是200,內(nèi)容是你請求的資源,同時有一個Last-Modified的屬性標(biāo)記此文件在服務(wù)期端最后被修改的時間,格式類似這樣:
Last-Modified: Fri, 12 May 2006 18:53:33 GMT
客戶端第二次請求此URL時,根據(jù) HTTP 協(xié)議的規(guī)定,瀏覽器會向服務(wù)器傳送 If-Modified-Since 報頭,詢問該時間之后文件是否有被修改過:
If-Modified-Since: Fri, 12 May 2006 18:53:33 GMT
如果服務(wù)器端的資源沒有變化,則自動返回 HTTP 304 (Not Changed.)狀態(tài)碼,內(nèi)容為空,這樣就節(jié)省了傳輸數(shù)據(jù)量。當(dāng)服務(wù)器端代碼發(fā)生改變或者重啟服務(wù)器時,則重新發(fā)出資源,返回和第一次請求時類似。從而保證不向客戶端重復(fù)發(fā)出資源,也保證當(dāng)服務(wù)器有變化時,客戶端能夠得到最新的資源。
ETag
HTTP 協(xié)議規(guī)格說明定義ETag為“被請求變量的實體值” 。另一種說法是,ETag是一個可以與Web資源關(guān)聯(lián)的記號(token)。典型的Web資源可以一個Web頁,但也可能是JSON或XML文檔。服務(wù)器單獨(dú)負(fù)責(zé)判斷記號是什么及其含義,并在HTTP響應(yīng)頭中將其傳送到客戶端,以下是服務(wù)器端返回的格式:
ETag: "50b1c1d4f775c61:df3"
客戶端的查詢更新格式是這樣的:
If-None-Match: W/"50b1c1d4f775c61:df3"
如果ETag沒改變,則返回狀態(tài)304然后不返回,這也和Last-Modified一樣。

2.3 一種緩存分級策略實例
- max-age=86400
瀏覽器和任何中繼緩存均可以將響應(yīng)(如果是public的)緩存長達(dá)一天(60 秒 x 60 分 x 24 小時) - private, max-age=600
客戶端瀏覽器只能將響應(yīng)緩存最長 10 分鐘(60 秒 x 10 分) - no-cache
通過ETag協(xié)商 - no-store
不允許緩存響應(yīng),每個請求必須獲取完整的響應(yīng)。
上面是一種緩存分級機(jī)制的栗子,可以根據(jù)資源的更新情況進(jìn)行響應(yīng)的配置。當(dāng)然還可以有更多靈活配置。
3 基于flask實現(xiàn)
3.1 cache-control的flask實現(xiàn)
flask有一個擴(kuò)展包解決這個問題:flask-cachecontrol。秉承flask的傳統(tǒng),使用的方法十分簡單(看看代碼也好)。
from flask.ext.cachecontrol import (
FlaskCacheControl,
cache,
cache_for,
dont_cache)
flask_cache_control = FlaskCacheControl()
flask_cache_control.init_app(app)
@app.route('/')
@cache_for(hours=3)
def index_view():
return render_template('index_template')
@app.route('/stats')
@cache(max_age=3600, public=True)
def stats_view():
return render_template('stats_template')
@app.route('/dashboard')
@dont_cache()
def dashboard_view():
return render_template('dashboard_template')
它簡化為了三個場景,將相關(guān)的配置都自動在響應(yīng)包中添加。例如,采用cache(max_age=3, public=False)的修飾器,返回的緩存頭包括了幾個配置參數(shù)。
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 1804
Cache-Control: proxy-revalidate, no-cache, no-store, must-revalidate, max-age=0
Server: Werkzeug/0.10.4 Python/2.7.10
Date: Fri, 05 Aug 2016 03:51:28 GMT
3.2 ETag/Last-Modified的flask實現(xiàn)
ETag沒有flask擴(kuò)展包,這里有一篇官方的文章介紹實現(xiàn)方法。對方法總結(jié)一下:
- 給flask.response.set_etag()做一個猴子補(bǔ)?。∕onkeypatching)。
- 猴子補(bǔ)丁的內(nèi)容為,校驗請求包的“IF-MATCH”與“IF-NONE-MATCH”信息(即請求包的ETag字段),如果不合法則直接返回錯誤;如果合法,則執(zhí)行校驗etag,將本次新生成的etag碼與請求包中的“IF-NONE-MATCH”碼相匹配,則拋出“NotModified”異常(執(zhí)行304狀態(tài)碼及空包返回),如果不匹配,則進(jìn)行全數(shù)據(jù)返回且包含了新的ETag信息。
_old_set_etag = werkzeug.ETagResponseMixin.set_etag
@functools.wraps(werkzeug.ETagResponseMixin.set_etag)
def _new_set_etag(self, etag, weak=False):
# only check the first time through; when called twice
# we're modifying
if (hasattr(flask.g, 'condtnl_etags_start') and
flask.g.condtnl_etags_start):
if flask.request.method in ('PUT', 'DELETE', 'PATCH'):
if not flask.request.if_match:
raise PreconditionRequired
if etag not in flask.request.if_match:
flask.abort(412)
elif (flask.request.method == 'GET' and
flask.request.if_none_match and
etag in flask.request.if_none_match):
raise NotModified
flask.g.condtnl_etags_start = False
_old_set_etag(self, etag, weak)
werkzeug.ETagResponseMixin.set_etag = _new_set_etag
- 校驗ETag的行為在API的響應(yīng)代碼中執(zhí)行。
app = flask.Flask(__name__)
d = {'a': 'This is "a".\n', 'b': 'This is "b".\n'}
@app.route('/<path>',
methods = ['GET', 'PUT', 'DELETE', 'PATCH'])
@conditional
def view(path):
try:
# SHA1 should generate well-behaved etags
etag = hashlib.sha1(d[path]).hexdigest()
if flask.request.method == 'GET':
response = flask.make_response(d[path])
response.set_etag(etag)
else:
response = flask.Response(status=204)
del response.headers['content-type']
response.set_etag(etag)
if flask.request.method == 'DELETE':
del d[path]
del response.headers['etag']
else:
if flask.request.method == 'PUT':
d[path] = flask.request.data
else: # (PATCH)
# lame PATCH technique
d[path] += flask.request.data
response.set_etag(hashlib.sha1(d[path])
.hexdigest())
return response
except KeyError:
flask.abort(404)
app.run()
4 總結(jié)一下
緩存是一個減緩服務(wù)端壓力的手段。對于一些很少改變的且不敏感的資源,可以用開放式緩存,讓CDN等中間環(huán)節(jié)也幫我們存信息。而對于一些少改變且稍為敏感的資源,則可以使用私有式緩存,讓客戶端瀏覽器執(zhí)行緩存。甚至于更新很頻繁的還可設(shè)置為ETag校驗或者數(shù)據(jù)十分敏感,不能緩存的也有no-store機(jī)制。
采用ETag可能是比較折衷的辦法,在減緩帶寬壓力上十分有效,但在減緩服務(wù)器計算壓力(甚至數(shù)據(jù)庫壓力)上仍然沒有太大意義(ETag要求服務(wù)端先獲取了數(shù)據(jù)之后,再生成ETag,再用ETag與請求包的ETag驗證)。
參考:
https://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html
http://www.tuicool.com/articles/YBbeM33
https://github.com/twiebe/Flask-CacheControl
https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=zh-cn
http://blog.csdn.net/salmonellavaccine/article/details/42734183
http://flask.pocoo.org/snippets/95/