ASP.NET Core ResponseCaching:基于 VaryByHeader 定制缓存 Key

ASP.NET Core ResponseCaching 提供了缓存http响应内容的能力,通过它可以在本地内存中直接缓存http响应内容,这是速度最快的服务端缓存,省却了网络传输与生成响应内容的开销,是 Memcached 或 Redis 等分布式缓存的有效补充。欲更多了解 ResponseCaching ,推荐阅读园子里的一篇博文 谈谈ASP.NET Core中的ResponseCaching

ResponseCaching 提供了 VaryByHeader 与 VaryByQueryKeys 这两种种方式配置缓存 Key ,我们在使用 VaryByHeader 时发现一个问题 —— 使用 VaryByHeader 时如果不做限制,会面临缓存被撑爆的风险。比如上面根据 Accept 请求头进行缓存,由于客户端可以任意修改请求头,如果有爬虫发出大量的请求,并且每个请求的 Accept 请求头不一样,ResponseCaching 会因此为每个请求生成缓存项,直至内存被耗尽。

为了避免这个问题,我们需要对基于请求头生成缓存 key 的规则进行限制,但 ResponseCaching 没有提供对应的定制能力。

我们开始想到的一个解决方法是基于适配器模式自己实现 IResponseCachingKeyProvider 接口,代码如下:

public class CustomResponseCachingKeyProvider : IResponseCachingKeyProvider
{
    private static readonly char KeyDelimiter = '\x1e';
    private ResponseCachingKeyProvider _responseCachingKeyProvider;

    public CustomResponseCachingKeyProvider(ResponseCachingKeyProvider responseCachingKeyProvider)
    {
        _responseCachingKeyProvider = responseCachingKeyProvider;
    }

    public string CreateBaseKey(ResponseCachingContext context)
    {
        return _responseCachingKeyProvider.CreateBaseKey(context);
    }

    public IEnumerable<string> CreateLookupVaryByKeys(ResponseCachingContext context)
    {
        return _responseCachingKeyProvider.CreateLookupVaryByKeys(context);
    }

    public string CreateStorageVaryByKey(ResponseCachingContext context)
    {
        var key = _responseCachingKeyProvider.CreateStorageVaryByKey(context);
        var accept = context.HttpContext.Request.GetTypedHeaders().Accept;
        if (accept.Any(x => x.MediaType == "application/json"))
        {   
            key += KeyDelimiter + "accept=application/json";
        }            
        else
        {
            key += KeyDelimiter + "accept=text/plain";
        }

        return key;
    }
}

然后在 Startup 的 ConfigureServices 方法中进行注册

services.AddTransient<ResponseCachingKeyProvider>();
services.AddSingleton<IResponseCachingKeyProvider, CustomResponseCachingKeyProvider>();
services.AddResponseCaching();

除了上面这两步之外,还要给 VaryByHeader 随便赋个值,因为 CreateStorageVaryByKey() 方法只有在 VaryByHeader 或 VaryByQueryKeys 有值的情况下才会被调用。

[ResponseCache(Duration = 30, VaryByHeader = "_")]

实现后觉得这不是一个优雅的解决方法。 

后来尝试修改 ResponseCaching 的源代码,但 ResponseCaching 在设计时并没有考虑到这个场景,修改工作量比较大。

再后来转念一想,不用这么麻烦,可以借助于已有的 VaryByHeader 机制,通过 middleware 根据客户端的请求头生成用于 VaryByHeader 的请求头,middleware 的代码如下

app.Use(async (context, next) =>
{
    var accept = context.Request.GetTypedHeaders().Accept;
    var mediaTypes = new string[] { "application/json", "text/html" };
    var mediaType = accept.Select(x => x.MediaType).FirstOrDefault(x => mediaTypes.Contains(x.Value));
    context.Request.Headers.Add("Accept-MediaType", mediaType == null ? "text/plain" : mediaType.Value);
    await next.Invoke();
});

ResponseCaching 属性的声明如下

[ResponseCache(Duration = 60, VaryByHeader = "Accept-MediaType", VaryByQueryKeys = new string[] { "id" })]

折腾了 1 天的问题换了个思路 10 分钟搞定。

另外,使用 ResponseCaching 还需要注意一个地方,如果用到了 CORS ,还需要在 VaryByHeader 中添加 "Origin"

[ResponseCache(Duration = 60, VaryByHeader = "Accept-MediaType,Origin", VaryByQueryKeys = new string[] { "id" })]
posted @ 2018-06-14 12:58 dudu 阅读(...) 评论(...) 编辑 收藏