本文所需的一些預備知識能夠看這裏: http://www.cnblogs.com/cgzl/p/9010978.html 和 http://www.cnblogs.com/cgzl/p/9019314.htmlhtml
創建Richardson成熟度2級的POST、GET、PUT、PATCH、DELETE的RESTful API請看這裏:http://www.javashuo.com/article/p-sryarofa-ct.html 和 http://www.javashuo.com/article/p-nmpufjil-ee.html 和 http://www.javashuo.com/article/p-oxgmtvqz-dz.htmlgit
HATEOAS:http://www.javashuo.com/article/p-evqneumz-dv.html。github
本文介紹緩存和併發,無需看前邊文章也能明白吧。web
本文所需的練習代碼(右鍵另存,後綴改成zip):https://images2018.cnblogs.com/blog/986268/201806/986268-20180611132306164-388387828.jpg數據庫
根據REST約束:「每一個響應都應該定義它本身是否能夠被緩存」。本文就要介紹如何保證HTTP響應是可被緩存的,這裏就要用到HTTP緩存的知識,HTTP緩存是HTTP標準的一部分(RFC 2616, RFC 7234)。後端
"除非性能能夠獲得很大的提高,不然用緩存是沒啥用的。HTTP/1.1裏緩存的目標就是在不少場景中能夠避免發送請求,在其餘狀況下避免返回完整的響應"。瀏覽器
針對避免發送請求的數量這一點,緩存使用了過時機制。緩存
針對避免返回完整響應這點,緩存採用了驗證機制。服務器
緩存是什麼?網絡
緩存是一個獨立的組件,存在於API和API消費者之間。
緩存接收API消費者的請求,並把請求發送給API;
緩存還從API接收響應而且若是響應是可緩存的就會把響應保存起來,並把響應返回給API的消費者。若是同一個請求再次發送,那麼緩存就可能會吧保存的響應返回給API消費者。
緩存能夠看做是請求--響應通信機制的中間人。
HTTP裏面有三種緩存:
過時模型讓服務器能夠聲明請求的資源也就是響應信息能保持多長時間是「新鮮」的狀態。緩存能夠存儲這個響應,因此後續的請求能夠由緩存來響應,只要緩存是「新鮮」的。處於這個目的,須要使用兩個Response Headers:
Expires Header,它包含一個HTTP日期,該日期表述了響應會在什麼時間過時,例如:Expires: Mon, 11 Jun 2018 13:55:41 GMT。可是它可能會存在一些同步問題,因此要求緩存和服務器的時間是保持一致的。它對響應的類型、時間、地點的控制頗有限,由於這些東西都是由cache-control這個Header來控制和限制的。
Cache-Control Header,例如Cache-Control: public, max-age=60,這個Header裏包含兩個指令public和max-age。max-age代表了響應能夠被緩存60秒,因此時鐘同步就不是問題了;而public則表示它能夠被共享和私有的緩存所緩存。因此說服務器能夠決定響應是否容許被網關緩存或代理緩存所緩存。對於過時模型,優先考慮使用Cache-Control這個Header。Cache-Control還有不少其它的指令,常見的幾個能夠在ASP.NET Core官網上看:https://docs.microsoft.com/en-us/aspnet/core/performance/caching/response?view=aspnetcore-2.1#http-based-response-caching
過時模型的工做原理,看下面的例子:
這裏的Cache 緩存能夠是私有的也能夠是共享的。
客戶端程序發送請求 GET countries,這時尚未緩存版本的響應,因此緩存會繼續把請求發送到API服務器;而後API返回響應給緩存,響應裏面包含了Cache-Control這個Header,Cache-Control聲明瞭響應會保持「新鮮」(或者叫有效)半個小時,最後緩存把響應返回給客戶端,但同時緩存複製了一份響應保存了起來。
而後好比10分鐘以後,客戶端又發送了同樣的請求:
這時,緩存裏的響應還在有效期內,緩存會直接返回這個響應,響應裏包含一個age Header,針對這個例子(10分鐘),age的值就是600(秒)。
這種狀況下,對API服務器的請求就被避免了,只有在緩存過時(或者叫不新鮮 Stale)的狀況下,緩存纔會訪問後端的API服務器。
若是緩存是私有的,例如在web應用的localstorage裏面,或者手機設備上,請求到此就中止了。
若是緩存是共享的,例如緩存在服務器上,狀況就不同了。
好比說10分鐘以後另外一個客戶端發送了一樣的請求,這個請求確定首先來到緩存這裏,若是緩存尚未過時,那麼緩存會直接把響應返回給客戶端,此次age Header的值就是1200(秒),20分鐘了:
總的來講私有緩存會減小網絡帶寬的需求,同時會減小從緩存到API的請求。
而共享緩存並不會節省緩存到API的網絡帶寬,可是它會大幅減小到API的請求。例如同時10000個客戶端發出了一樣請求到API,第一個到達的請求會來到API程序這裏,而其它的一樣請求只會來到緩存,這也意味着代碼的執行量會大大減小,訪問數據庫的次數也會大大減小,等等。
因此組合使用私有緩存和共享緩存(客戶端緩存和公共/網關緩存)仍是不錯的。可是這種緩存仍是更適用於比較靜態的資源,例如圖片、內容網頁;而對於數據常常變化的API並不太合適。若是API添加了一條數據,那麼針對這10000個客戶端,所緩存的數據就不對了,針對這個例子有可能半個小時都會返回不正確的數據,這時就須要用到驗證模型了。
驗證模型用於驗證緩存的響應數據是不是保持最新的。
這種狀況下,當被緩存的數據將要成爲客戶端請求的響應的時候,它首先會檢查一下源服務器或者擁有最新數據的中間緩存,看看它所緩存的數據是否仍然最新。這裏就要用到驗證器。
驗證器分爲兩種:強驗證器,弱驗證器。
強驗證器:若是響應的body或者header發生了變化,強驗證器就會變化。典型的例子就是ETag(Entity Tag)響應header,例如:ETag: "12345678",ETag是由Web服務器或者API發配的不透明標識,它表明着某個資源的特定版本。強驗證器能夠在任意帶有緩存的上下文中使用,在更新資源的時候強驗證器能夠用來作併發檢查。
弱驗證器:當響應變化的時候,弱驗證器一般不必定會變化,由服務器來決定何時變化,一般的作法有「只有在重要變化發生的時候才變化」。一個典型的例子就是Last-Modified(最後修改時間)這個Header ,例如:Mon, 11 Jun 2018 13:55:41 GMT,它裏面包含着資源最後修改的時間,這個就有點弱,由於它精確到秒,由於有可能一秒內對資源進行兩次以上的更新。但即便針對弱驗證器,時鐘也必須同步,因此它和expires header有一樣的問題,因此ETag是更好的選擇。
還有一種弱ETag,它以w/開頭,例如ETag: "w/123456789",它被看成弱驗證器來對待,可是仍是由服務器來決定其程度。當ETag是這種格式的時候,若是響應有變化,它不必定就變化。
弱驗證器只有在容許等價(大體相等)的狀況下可已使用,而在要求徹底相等的需求下是不可使用的。
HTTP標準建議若是可能的話最好仍是同時發送ETag和Last-Modified這兩個Header。
下面看看其工做原理。客戶端第一次請求的時候,請求到達緩存後發現緩存裏沒有,而後緩存把請求發送到API;API返回響應,這個響應包含ETag和Last-Modified 這兩個Header,響應被髮送到緩存,而後緩存再把它發送給客戶端,與此同時緩存保存了這個響應的一個副本。
10分鐘後,客戶端再次發送了一樣的請求,請求來到緩存,可是沒法保證緩存的響應是「新鮮」的,這個例子裏並無使用Cache-Control Header,因此緩存就必須到服務器的API去作檢查。這時它會添加兩個Headers:If-None-Match,它被設爲已緩存響應數據的ETag的值;If-Modified-Since,它被設爲已緩存響應數據的Last-Modified的值。如今這個請求就是根據狀況而定的了,服務器接收到這個請求並會根據證器來比較這些header或者生成響應。
若是檢查合格,服務器就不須要生成響應了,它會返回304 Not Modified,而後緩存會返回緩存的響應,這個響應還包含了一個最新的Last-Modified Header(若是支持Last-Modifed的話);
而若是響應的資源發生變化了,API就會生成新的響應。
若是是私有緩存,那就請求就會停在這。
但若是是共享緩存的話,假如10分鐘以後另外一個客戶端發送了請求,這個請求也會到達緩存,而後跟上面同樣的流程:
總的來講就是,一樣的響應只會被生成一次。
對比一下:
私有緩存:後續的請求會節省網絡帶寬,咱們須要與API進行通訊,可是API不須要把完整的響應返回來,若是資源沒有變化的話只須要返回304便可。
共享緩存:會節省緩存和API之間的帶寬,若是驗證經過的話,API不須要從新生成響應而後從新發送回來。
過時模型和驗證模型仍是常常被組合使用的。
能夠這樣作:
若是使用私有緩存,這時只要響應沒有過時,那麼響應直接會從私有緩存返回。這樣作的好處就是減小了與API之間的通訊,也減小了API生成響應的工做,減輕了帶寬需求。而若是私有緩存過時了,那仍是會訪問到API的。若是隻有過時(模型)檢查的話,這就意味着若是過時了API就得從新生成響應。可是若是使用驗證(模型)檢查的話,咱們可能就會避免這種狀況。由於緩存的響應過時了並不表明緩存的響應就不是有效的了,API會檢查驗證器,若是響應依然有效,就會返回304。這樣網絡帶寬和響應的生成動做都有可能被大幅度減小了。
若是是共享緩存,緩存的響應只要沒過時就會一直被返回,這樣雖然不會節省客戶端和緩存之間的網絡帶寬,可是會節省緩存和API之間的網絡帶寬,同時也大幅度減小了到API的請求次數,這個要比私有緩存幅度大,由於共享緩存是共享與多是全部的客戶端的。若是緩存的響應過時了,緩存就必須與API通訊,但這也不必定就意味着響應必須被從新生成。若是驗證成功,就會返回304,沒有響應body,這就有可能減小了緩存和API之間的網絡帶寬需求,響應仍是從緩存返回到客戶端的。
因此綜上,客戶端配備私有緩存,服務器級別配備共享緩存就應該是最佳的實踐。
先看一下響應的Cache-Control經常使用指令:
上面這些都是由服務器決定的, 可是客戶端能夠覆蓋其中的一些設定.
請求的Cache-Control經常使用指令:
到目前也介紹了幾個指令了, 其實大多數狀況下使用max-age和public, private便可...
更多指令請查看: https://tools.ietf.org/html/rfc7234#section-5.2
根據REST的約束, 爲了支持HTTP緩存, 咱們須要一個能夠生成正確的響應Header的組件, 而且能夠檢查發送的請求的Header, 因此咱們能夠返回304 Not Modified或者412 Preconditioned Failed.
這個組件應該位於緩存的後端, ASP.NET Core裏有個自帶的屬性標籤 [ResponseCache] (https://docs.microsoft.com/en-us/aspnet/core/performance/caching/response?view=aspnetcore-2.1#responsecache-attribute), 它能夠應用於Controller的Actions. 爲設定適當響應緩存Header它能夠指定所需的參數. 它只能作這些, 沒法在緩存裏存儲響應, 它並非緩存存儲. 並且由於它好像不支持ETag, 因此暫時先不使用這個.
能夠考慮CacheCow,它能夠生成ETag,也支持.NET Core,可是它並無內置中間件來返回304。因此我這裏使用的是Marvin.Cache.Headers。
安裝:
Startup的ConfigureServices方法裏配置:
這裏還能夠配置Header的生成選項,但暫時先使用默認的配置。
而後在Configure方法裏,把這個中間件添加在app.useMvc()以前:
這裏就是處理並返回304的邏輯。
還須要設置一下Postman, 要保證Send no-cache header這一項是off的:
發送請求測試:
這是第一次訪問,會執行Action方法,而後返回響應。響應的Header如上圖所示,裏面包含了緩存相關的Header。
默認的Cache-Control是public,max-age是60秒。Expires header也反映了過時的時間,也就是1分鐘以後。
用於驗證的ETag和Last-Modified也被生成和添加了,Last-Modified就是如今的時間。
ETag的生成邏輯並非標準的一部分,這個能夠由咱們本身來決定。當讓響應是等價的仍是徹底相等的也是由咱們來決定。
默認狀況下,這個中間件會考慮到請求路徑、Accept、Accept-language 這些Header以及響應的body。
再次發送該請求,因爲已經超過了1分鐘,因此仍是會走Action方法的:
而後在1分鐘以內再次發送請求:
仍是走了這個Action方法!!
Header仍是有變化的。
這個現象是沒有問題的,由於這個庫只是負責生成Header和驗證,它並非緩存存儲器。
想要緩存數據,那就須要一個緩存存儲器了,能夠是私有、公共的也能夠是二者兼顧的。這個一會再說。
先來看看驗證,若是一個響應是不新鮮的(過時的),咱們知道這樣話緩存必須進行從新驗證,最好是用ETag進行驗證,他會把ETag的值賦給If-None-Match這個Header:
這時就會返回304 Not Modified,而Action方法也不會執行。
下面測試一下PUT動做:
更新數據以後,我再發送一次以前的GET請求:
此次Action方法又被執行了,這說明驗證失敗了,由於ETag已經不一致了,當我發送PUT請求的時候,生成了一個新的ETag。
咱們也能夠對如何生成Header進行配置,打開Startup的ConfigureServices方法:
配置參數仍是不少的,這裏我分別爲過時模型和驗證模型修改了一個參數。
過時模型的max-age設爲600秒。驗證模型爲Cache-Control添加了must-revalidate指令,也就是說若是緩存的響應過時了,那麼必須進行從新驗證。
再次發送那個GET請求:
從新執行了Action方法,也能夠看到響應Header的變化。
以前只是生成了緩存相關的Header,尚未進行真正的存儲,如今就介紹存儲這部分。
緩存有私有的、共享的等。
私有的不在咱們討論的範圍內,由於它在客戶端。
私有和共享緩存,有一些緩存是二者的混合,根據你在哪使用它來決定給其類型。例如CacheCow。
微軟提供了一個共享緩存,支持.NET Core:ResponseCaching中間件(https://docs.microsoft.com/en-us/aspnet/core/performance/caching/middleware?view=aspnetcore-2.1)。
這個中間件會檢查Marvin.Cache.Headers這個中間件生成的Header,並把響應放到緩存並根據Header把它們服務給客戶端,可是ResponseCaching中間件它本身並不會生成這些Header。
在ConfigureServices裏註冊:
而後在Configure方法裏,把這個緩存存儲添加到管道:
注意順序,要保證它在UseHttpCacheHeaders()以前。
測試,發送GET請求:
此次會執行Action方法,返回響應。
再次發送GET請求:
此次沒有走進Action方法裏,而是從緩存返回的,這裏還多了一個Age header,它告訴了我響應的」年齡「,他已經活了123秒了。
再次請求:
年齡變成了243秒,仍是小於600秒。很顯然這提升了應用的性能。。。
到目前咱們能夠生成Cache-Control和Etag的Headers了,可是尚未用到ETag的另外一個功能:
看下面這個狀況,很常見:
兩個客戶端1和2,客戶1先獲取了id爲1的Country資源,隨後客戶2也獲取了這個資源;而後客戶2對資源進行了修改,先進行了PUT動做進行更新,而後客戶1才修改好Country而後PUT到服務器。
這時客戶1就會把客戶2的更改徹底覆蓋掉,這是個常見問題。
針對這樣的問題,咱們須要使用一些處理併發衝突的策略:悲觀併發控制和樂觀併發控制。
悲觀併發控制意味着資源是爲客戶1鎖定的,只要資源處於鎖定的狀態,別人就不能修改它,只有客戶1能夠修改它。可是悲觀併發控制是沒法在REST下實現的,由於REST有個無狀態約束。
樂觀併發控制,這就意味着客戶1會獲得一個Token,並容許他更新資源,只要Token是合理有效的,那麼客戶1就一直能夠更新該資源。在REST裏這是能夠實現的,而這個Token就是個驗證器,並且要求是強驗證器,因此咱們能夠用ETag。
回到例子:
客戶1發送GET請求,返回響應並帶着ETag Header。而後客戶2發送一樣的請求,返回一樣的響應和Etag。
客戶2先進行更新,並把Etag的值賦給了If-Match Header,API檢查這個Header並和它爲這個響應所保存的ETag值進行比較,這時針對這個響應會生成新的ETag,響應包含着這個新的ETag。
而後客戶1進行PUT更新操做,它的If-Match Header的值是客戶1以前獲得的ETag的值,在到達API以後,API就知道這個和資源最新的ETag的值不同,因此API會返回412 Precondition Failed。
因此客戶1的更新沒有成功,由於它使用的是老版本的資源。這就是樂觀併發控制的工做原理。
下面看測試,
客戶1先GET:
客戶2GET:
注意他們兩個的ETag是同樣的。
而後客戶2先更新:
最後客戶1再更新(使用的是老的ETag):
返回412。
本文比較短,一些關於緩存技術的內容並無寫,距離REST的主題有點遠。
ASP.NET Core關於緩存部分的文檔在這裏:https://docs.microsoft.com/en-us/aspnet/core/performance/caching/?view=aspnetcore-2.1
本系列的源碼在:https://github.com/solenovex/ASP.NET-Core-2.0-RESTful-API-Tutorial