我們利用ASP.NET開發的大部分API都是為了對外提供資源,對于不易變化的資源內容,針對某個維度對其實施緩存可以很好地提供應用的性能。《內存緩存與分布式緩存的使用》介紹的兩種緩存框架(本地內存緩存和分布式緩存)為我們提供了簡單易用的緩存讀寫編程模式,本篇介紹的則是針對針對HTTP響應內容實施緩存,ResponseCachingMiddleware中間件賦予我們的能力[本文節選《ASP.NET Core 6框架揭秘》第22章]。
目錄
[S2201]基于路徑的響應緩存(源代碼)
[S2202]基于指定的查詢字符串緩存響應(源代碼)
[S2203]基于指定的請求報頭緩存響應(源代碼)
[S2204]緩存屏蔽(源代碼)
[S2201]基于路徑的響應緩存
為了確定響應內容是否被緩存,如下的演示程序針對路徑“/{foobar?}”注冊的中間件會返回當前的時間。如代碼片段所示,我們調用UseResponseCaching擴展方法對ResponseCachingMiddleware中間件進行了注冊, AddResponseCaching擴展方法則注冊了該中間件依賴的服務。
using?Microsoft.Net.Http.Headers;var?app?=?WebApplication.Create();
app.UseResponseCaching();
app.MapGet("/{foobar}",?Process);
app.Run();static?DateTimeOffset?Process(HttpResponse?response)
{response.GetTypedHeaders().CacheControl?=?new?CacheControlHeaderValue{Public?=?true,MaxAge?=?TimeSpan.FromSeconds(3600)};return?DateTimeOffset.Now;
}
終結點處理方法Process在返回當前時間之前添加了一個Cache-Control響應報頭,并且將它的值設置為“public, max-age=3600”(public表示緩存的是可以被所有用戶共享的公共數據,而max-age則表示過期時限,單位為秒)。要證明整個響應的內容是否被緩存,只需要驗證在緩存過期之前具有相同路徑的多個請求對應的響應是否具有相同的主體內容。
GET?http://localhost:5000/foo?HTTP/1.1
Host:?localhost:5000HTTP/1.1?200?OK
Content-Type:?application/json;?charset=utf-8
Date:?Tue,?14?Dec?2021?02:13:39?GMT
Server:?Kestrel
Cache-Control:?public,?max-age=3600
Content-Length:?35"2021-12-14T10:13:39.8838806+08:00"
GET?http://localhost:5000/foo?HTTP/1.1
Host:?localhost:5000HTTP/1.1?200?OK
Content-Type:?application/json;?charset=utf-8
Date:?Tue,?14?Dec?2021?02:13:39?GMT
Server:?Kestrel
Age:?3
Cache-Control:?public,?max-age=3600
Content-Length:?35"2021-12-14T10:13:39.8838806+08:00"
GET?http://localhost:5000/bar?HTTP/1.1
Host:?localhost:5000HTTP/1.1?200?OK
Content-Type:?application/json;?charset=utf-8
Date:?Tue,?14?Dec?2021?02:13:49?GMT
Server:?Kestrel
Cache-Control:?public,?max-age=3600
Content-Length:?35"2021-12-14T10:13:49.0153031+08:00"
GET?http://localhost:5000/bar?HTTP/1.1
Host:?localhost:5000HTTP/1.1?200?OK
Content-Type:?application/json;?charset=utf-8
Date:?Tue,?14?Dec?2021?02:13:49?GMT
Server:?Kestrel
Age:?2
Cache-Control:?public,?max-age=3600
Content-Length:?35"2021-12-14T10:13:49.0153031+08:00"
如下所示的四組請求和響應是在不同時間發送的,其中兩個和后兩個請求采用的請求路徑分別為“/foo”和“/bar”。可以看出采用相同路徑的請求會得到相同的時間戳,意味著后續請求返回的內容來源于緩存,并且說明了響應內容默認是基于請求路徑進行緩存的。由于請求發送的時間不同,所以返回的緩存副本的“年齡”(對應響應報頭Age)也是不同的。
[S2202]基于指定的查詢字符串緩存響應
一般來說,對于提供資源的API來說,請求的路徑可以作為資源的標識,所以請求路徑決定返回的資源,這也是響應基于路徑進行緩存的理論依據。但是在很多情況下,請求路徑僅僅是返回內容的決定性因素之一,即使路徑能夠唯一標識返回的資源,但是資源可以采用不同的語言來表達,也可以采用不同的編碼方式,所以最終的響應的內容還是不一樣的。在編寫請求處理程序的時候,我們還經常根據請求攜帶的查詢字符串來生成響應的內容。以我們的演示的返回當前時間戳的實例來說,我們可以利用請求攜帶的查詢字符串“utc”或者請求報頭“X-UTC”來決定返回的是本地時間還是UTC時間。
using?Microsoft.AspNetCore.Mvc;
using?Microsoft.Net.Http.Headers;var?app?=?WebApplication.Create();
app.UseResponseCaching();
app.MapGet("/{foobar?}",?Process);
app.Run();static?DateTimeOffset?Process(HttpResponse?response,[FromHeader(Name?=?"X-UTC")]?string??utcHeader,[FromQuery(Name?="utc")]string??utcQuery)
{response.GetTypedHeaders().CacheControl?=?new?CacheControlHeaderValue{Public?=?true,MaxAge?=?TimeSpan.FromSeconds(3600)};return?Parse(utcHeader)????Parse(utcQuery)????false??DateTimeOffset.UtcNow?:?DateTimeOffset.Now;static?bool??Parse(string??value)=>?value?==?null??null:?string.Compare(value,?"1",?true)?==?0?||?string.Compare(value,?"true",?true)?==?0;
}
由于響應緩存默認采用的Key是派生于請求的路徑,但是對于我們修改過的這個程序來說,默認的這個緩存鍵的生成策略就有問題了。程序啟動后,我們采用路徑“/foobar”發送了如下兩個請求,其中第一個請求返回了實時生成的本地時間(+08:00表示北京時間采用的時區),對于第二個情況下,我們本來希望指定“utc”查詢字符串以返回一個UTC時間,但是我們得到卻是緩存的本地時間。
GET?http://localhost:5000/foobar?HTTP/1.1
Host:?localhost:5000HTTP/1.1?200?OK
Content-Type:?application/json;?charset=utf-8
Date:?Tue,?14?Dec?2021?02:54:54?GMT
Server:?Kestrel
Cache-Control:?public,?max-age=3600
Content-Length:?35"2021-12-14T10:54:54.6845646+08:00"
GET?http://localhost:5000/foobar?utc=true?HTTP/1.1
Host:?localhost:5000HTTP/1.1?200?OK
Content-Type:?application/json;?charset=utf-8
Date:?Tue,?14?Dec?2021?02:54:54?GMT
Server:?Kestrel
Age:?7
Cache-Control:?public,?max-age=3600
Content-Length:?35"2021-12-14T10:54:54.6845646+08:00"
[S2203]基于指定的請求報頭緩存響應
要解決這個問題,必須要讓我們希望的緩存維度作為緩存鍵的組成部分。就我們演示程序來說,就是得讓響應緩存的Key不僅僅包括請求的路徑,還應該包括查詢字符串“utc”和請求報頭“X-UTC”的值。為此我們對演示的程序進行了相應的修改。如下面的代碼片段所示,我們從當前HttpContext上下文中提取出IResponseCachingFeature特性,并將設置了它的VaryByQueryKeys屬性使之包含了參與緩存的查詢字符串的名稱“utc”。為了讓自定義請求報頭“X-UTC”的值也參與緩存,我們將“X-UTC”作為Vary響應報頭的值。
using?Microsoft.AspNetCore.Mvc;
using?Microsoft.AspNetCore.ResponseCaching;
using?Microsoft.Net.Http.Headers;var?app?=?WebApplication.Create();
app.UseResponseCaching();
app.MapGet("/{foobar?}",?Process);
app.Run();static?DateTimeOffset?Process(HttpContext?httpContext,[FromHeader(Name?=?"X-UTC")]?string??utcHeader,[FromQuery(Name?="utc")]string??utcQuery)
{var?response?=?httpContext.Response;response.GetTypedHeaders().CacheControl?=?new?CacheControlHeaderValue{Public?=?true,MaxAge?=?TimeSpan.FromSeconds(3600)};var?feature?=?httpContext.Features.Get<IResponseCachingFeature>()!;feature.VaryByQueryKeys?=?new?string[]?{?"utc"?};response.Headers.Vary?=?"X-UTC";return?Parse(utcHeader)????Parse(utcQuery)????false???DateTimeOffset.UtcNow?:?DateTimeOffset.Now;static?bool??Parse(string??value)=>?value?==?null??null:?string.Compare(value,?"1",?true)?==?0?||?string.Compare(value,?"true",?true)?==?0;
}
對于我們修正過演示程序來說,請求查詢字符串“utc”的值會作為響應緩存鍵的一部分,我們在重啟應用后發送了如下針對“/foobar”的四個請求。前兩個請求和后兩個請求采用相同的查詢字符串(“?utc=true”和“?utc=false”),所以后一個請求會返回緩存的內容。
GET?http://localhost:5000/foobar?utc=true?HTTP/1.1
Host:?localhost:5000HTTP/1.1?200?OK
Content-Type:?application/json;?charset=utf-8
Date:?Tue,?14?Dec?2021?02:59:23?GMT
Server:?Kestrel
Cache-Control:?public,?max-age=3600
Vary:?X-UTC
Content-Length:?35"2021-12-14T02:59:23.0540999+00:00"
GET?http://localhost:5000/foobar?utc=true?HTTP/1.1
Host:?localhost:5000HTTP/1.1?200?OK
Content-Type:?application/json;?charset=utf-8
Date:?Tue,?14?Dec?2021?02:59:23?GMT
Server:?Kestrel
Age:?3
Cache-Control:?public,?max-age=3600
Vary:?X-UTC
Content-Length:?35"2021-12-14T02:59:23.0540999+00:00"
從上面給出的報文的內容可以看出,響應報文具有一個值為“X-UTC”的Vary報頭,它告訴客戶端響應的內容會根據這個名為“X-UTC”的請求報頭進行緩存。為了驗證這一點,我們在重啟應用后針對“/foobar”發送了如下四個請求,前兩個請求和后兩個請求采用相同的X-UTC(“X-UTC: True”和“X-UTC: False”),所以后一個請求會返回緩存的內容。
GET?http://localhost:5000/foobar?HTTP/1.1
X-UTC:?True
Host:?localhost:5000HTTP/1.1?200?OK
Content-Type:?application/json;?charset=utf-8
Date:?Tue,?14?Dec?2021?03:05:06?GMT
Server:?Kestrel
Cache-Control:?public,?max-age=3600
Vary:?X-UTC
Content-Length:?34"2021-12-14T03:05:06.977078+00:00"
GET?http://localhost:5000/foobar?HTTP/1.1
X-UTC:?True
Host:?localhost:5000HTTP/1.1?200?OK
Content-Type:?application/json;?charset=utf-8
Date:?Tue,?14?Dec?2021?03:05:06?GMT
Server:?Kestrel
Age:?3
Cache-Control:?public,?max-age=3600
Vary:?X-UTC
Content-Length:?34"2021-12-14T03:05:06.977078+00:00"
GET?http://localhost:5000/foobar?HTTP/1.1
X-UTC:?False
Host:?localhost:5000HTTP/1.1?200?OK
Content-Type:?application/json;?charset=utf-8
Date:?Tue,?14?Dec?2021?03:05:17?GMT
Server:?Kestrel
Cache-Control:?public,?max-age=3600
Vary:?X-UTC
Content-Length:?35"2021-12-14T11:05:17.0068036+08:00"
GET?http://localhost:5000/foobar?HTTP/1.1
X-UTC:?False
Host:?localhost:5000HTTP/1.1?200?OK
Content-Type:?application/json;?charset=utf-8
Date:?Tue,?14?Dec?2021?03:05:17?GMT
Server:?Kestrel
Age:?19
Cache-Control:?public,?max-age=3600
Vary:?X-UTC
Content-Length:?35"2021-12-14T11:05:17.0068036+08:00"
響應緩存通過復用已經生成的響應內容來提升性能,但不意味任何請求都適合以緩存的內容予以回復,請求攜帶的一些報頭會屏蔽掉響應緩存。或者更加準確的說法是,客戶端請求攜帶的一些報頭會“提醒”服務端當前場景需要返回實時內容。比如攜帶Authorization報頭的請求默認情況下將不會使用緩存的內容予以回復,下面的請求/響應體現了這一點。
GET?http://localhost:5000/foobar?HTTP/1.1
Host:?localhost:5000HTTP/1.1?200?OK
Content-Type:?application/json;?charset=utf-8
Date:?Tue,?14?Dec?2021?03:13:10?GMT
Server:?Kestrel
Cache-Control:?public,?max-age=3600
Vary:?X-UTC
Content-Length:?35"2021-12-14T11:13:10.4605924+08:00"
GET?http://localhost:5000/foobar?HTTP/1.1
Authorization:?foobar
Host:?localhost:5000HTTP/1.1?200?OK
Content-Type:?application/json;?charset=utf-8
Date:?Tue,?14?Dec?2021?03:13:17?GMT
Server:?Kestrel
Cache-Control:?public,?max-age=3600
Vary:?X-UTC
Content-Length:?35"2021-12-14T11:13:18.0918033+08:00"
關于Authorization請求報頭與緩存的關系,它與前面介紹的根據指定的請求報頭對響應內容進行緩存是不一樣的,當ResponseCachingMiddleware中間件在處理請求時,只要請求攜帶了此報頭,緩存策略將不再使用。如果客戶端對數據的實時性要求很高,那么它更希望服務總是返回實時生成的內容,這種情況下它利用利用攜帶的一些請求報頭向服務端傳達這樣的意圖,此時一般會使用到報頭“Cache-Control:no-cache”或者“Pragma:no-cache”。這兩個請求報頭對響應緩存的屏蔽作用體現在如下所示的四組請求/響應中。
GET?http://localhost:5000/foobar?HTTP/1.1
Host:?localhost:5000HTTP/1.1?200?OK
Content-Type:?application/json;?charset=utf-8
Date:?Tue,?14?Dec?2021?03:15:16?GMT
Server:?Kestrel
Cache-Control:?public,?max-age=3600
Vary:?X-UTC
Content-Length:?34"2021-12-14T11:15:16.423496+08:00"
GET?http://localhost:5000/foobar?HTTP/1.1
Cache-Control:?no-cache
Host:?localhost:5000HTTP/1.1?200?OK
Content-Type:?application/json;?charset=utf-8
Date:?Tue,?14?Dec?2021?03:15:26?GMT
Server:?Kestrel
Cache-Control:?public,?max-age=3600
Vary:?X-UTC
Content-Length:?35"2021-12-14T11:15:26.7701298+08:00"
GET?http://localhost:5000/foobar?HTTP/1.1
Pragma:?no-cache
Host:?localhost:5000HTTP/1.1?200?OK
Content-Type:?application/json;?charset=utf-8
Date:?Tue,?14?Dec?2021?03:15:36?GMT
Server:?Kestrel
Cache-Control:?public,?max-age=3600
Vary:?X-UTC
Content-Length:?35"2021-12-14T11:15:36.5283536+08:00"