標籤:針對 而且 特定 要求 最好 用戶端程式 base 同步問題 .com
https://www.cnblogs.com/cgzl/p/9165388.html
文所需的一些預備知識可以看這裡: http://www.cnblogs.com/cgzl/p/9010978.html 和 http://www.cnblogs.com/cgzl/p/9019314.html
建立Richardson成熟度等級2級的POST、GET、PUT、PATCH、DELETE的RESTful API請看這裡:https://www.cnblogs.com/cgzl/p/9047626.html 和 https://www.cnblogs.com/cgzl/p/9080960.html 和 https://www.cnblogs.com/cgzl/p/9117448.html
HATEOAS:https://www.cnblogs.com/cgzl/p/9153749.html。
本文介紹緩衝和並發,無需看前邊文章也能明白吧。
本文所需的練習代碼(右鍵另存,尾碼改為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裡面有三種緩衝:
- 用戶端緩衝/瀏覽器緩衝,它存在於用戶端,並且是私人的(因為它不會與其它用戶端共用)。
- 網關緩衝,它是共用的緩衝,位於伺服器端,所有的API消費者用戶端都會共用這個緩衝。它的別名還有反向 Proxy伺服器緩衝,HTTP加速器等。
- 代理緩衝,它位於網路上,共用的,它既不位於API消費者用戶端,也不在API伺服器上,它在網路的其它地方。這種緩衝經常被大型企業或ISP使用,用來服務大規模的使用者。(這個不介紹了,我不會)
到期模型
到期模型讓伺服器可以聲明請求的資源也就是響應資訊能保持多長時間是“新鮮”的狀態。緩衝可以儲存這個響應,所以後續的請求可以由緩衝來響應,只要緩衝是“新鮮”的。處於這個目的,需要使用兩個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定義了響應的生命期, 超過了這個值, 緩衝的響應就到期了, 它的單位是秒.
- s-maxage對於共用快取來說它會覆蓋max-age的值. 所以在私人緩衝和共用快取裡響應的到期時間可能會不同.
- 儲存地點:
- public, 它表示響應可以被任何一個緩衝器所緩衝, 私人或者共用的都可以.
- private, 它表示整個或部分響應的資訊是為某一個使用者所準備的, 並且不可以被共用的緩衝器所緩衝.
- 驗證:
- no-cache, 它表示在沒有和原始伺服器重新驗證之前, 響應不可以被後續的請求所使用.
- must-revalidate, 使用它伺服器可以聲明響應是否已經不新鮮了(到期了), 那麼就需要進行重新驗證. 這就允許伺服器強制讓緩衝進行重新驗證, 即使用戶端認為到期的響應也是可以的.
- proxy-revalidate, 他和must-revalidate差不多, 但不適用於私人緩衝.
- 其它:
- no-store, 它表示緩衝不允許儲存訊息的任何部分.
- no-transform, 它表示緩衝不可以對響應body的媒體類型進行轉換.
上面這些都是由伺服器決定的, 但是用戶端可以覆蓋其中的一些設定.
請求的Cache-Control常用指令:
- 新鮮度:
- max-age, 它表示用戶端不想要接收已經超過這個值的有效期間的響應
- min-fresh, 它表示用戶端可以接受到這樣的響應, 它的有效期間不小於它當前的年齡加上這個設定的值(秒), 也就是說用戶端想要響應還可以在指定的時間內保持新鮮.
- max-stale, 它表示用戶端可以接收到期的響應.
- 驗證:
- no-cache, 它表示緩衝不可以用儲存的響應來滿足請求. 原始伺服器需要重新驗證成功並產生響應.
- 其他:
- no-store, 和響應的一樣.
- no-transform, 和響應的一樣.
- only-if-cached, 它表示用戶端只想要緩衝的響應, 並且不要和原始伺服器進行重新驗證和產生. 這個比較適用於網路狀態非常差的狀態.
到目前也介紹了幾個指令了, 其實大多數情況下使用max-age和public, private即可...
更多指令請查看: https://tools.ietf.org/html/rfc7234#section-5.2
Cache Headers
根據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
本系列的源碼在:https://github.com/solenovex/ASP.NET-Core-2.0-RESTful-API-Tutorial
相關文章:
用ASP.NET Core 2.0 建立規範的 REST API -- 預備知識
用ASP.NET Core 2.0 建立規範的 REST API -- 預備知識 (2) + 準備項目
用ASP.NET Core 2.0 建立規範的 REST API -- GET 和 POST
用ASP.NET Core 2.0 建立規範的 REST API -- DELETE, UPDATE, PATCH 和 Log
用ASP.NET Core 2.1 建立規範的 REST API -- 翻頁/排序/過濾等
用ASP.NET Core 2.1 建立規範的 REST API -- HATEOAS
用ASP.NET Core 2.1 建立規範的 REST API -- 緩衝和並發