我们利用ASP.NET开发的大部分API都是为了对外提供资源,对于不易变化的资源内容,针对某个维度对其实施缓存可以很好地提供应用的性能。《内存缓存与分布式缓存的使用》介绍的两种缓存框架(本地内存缓存和分布式缓存)为我们提供了简单易用的缓存读写编程模式,本篇介绍的则是针对针对HTTP响应内容实施缓存,ResponseCachingMiddleware中间件赋予我们的能力(本文提供的示例演示已经同步到《ASP.NET Core 6框架揭秘-实例演示版》)。
目录
[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:5000 HTTP/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:5000 HTTP/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:5000 HTTP/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:5000 HTTP/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:5000 HTTP/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:5000 HTTP/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()!; 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:5000 HTTP/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:5000 HTTP/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:5000 HTTP/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:5000 HTTP/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:5000 HTTP/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:5000 HTTP/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"
[S2204]缓存屏蔽
响应缓存通过复用已经生成的响应内容来提升性能,但不意味任何请求都适合以缓存的内容予以回复,请求携带的一些报头会屏蔽掉响应缓存。或者更加准确的说法是,客户端请求携带的一些报头会“提醒”服务端当前场景需要返回实时内容。比如携带Authorization报头的请求默认情况下将不会使用缓存的内容予以回复,下面的请求/响应体现了这一点。
GET http://localhost:5000/foobar HTTP/1.1 Host: localhost:5000 HTTP/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:5000 HTTP/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:5000 HTTP/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:5000 HTTP/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:5000 HTTP/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"