系列导航及源代码

需求

有的时候为了减少客户端请求相同资源的逻辑重复执行,我们会考虑使用一些缓存的方式,在.NET 6中,我们可以借助框架提供的中间件来实现请求资源的缓存。

目标

实现请求结果的缓存。

原理与思路

对于在.NET6中实现缓存,我们可以使用响应缓存中间件ResponseCaching来实现,同时可以使用Marvin.Cache.Headers来为我们提供更多的缓存相关的属性。

实现

使用原生ResponseCaching实现缓存

既然是中间件,我们便在Program中引入:

  • Program.cs
// 省略其他...
// 配置缓存中间件
builder.Services.AddResponseCaching();
builder.Services.AddControllers();
// ...
// 使用缓存中间件
app.UseResponseCaching();
app.MapControllers();

在使用方法上,有几种方式可以实现配置:1)进行全局的配置,应用于所有添加了相同ProfileNameResponseCache的Controller响应;2)对单个Controller/Action进行配置,应用于当前作用的Controller/Action;3)全局配置后,由单个Controller/Action覆盖全局配置。我们会演示1)和3)的场景。

我们准备使用获取所有TodoLists的接口进行演示。

先看如何进行全局配置:

  • Program.cs
// 省略其他...
builder.Services.AddControllers(options =>
{
options.CacheProfiles.Add("60SecondDuration", new CacheProfile { Duration = 60 });
});

验证1: 全局配置Caching

首先给我们要进行验证的Action添加属性:

  • TodoListController.cs
// 省略其他...
[HttpGet]
[ResponseCache(CacheProfileName = "60SecondDuration")]
public async Task<ApiResponse<List<TodoListBriefDto>>> Get()
{
return ApiResponse<List<TodoListBriefDto>>.Success(await _mediator.Send(new GetTodosQuery()));
}

启动Api项目,第一次执行获取TodoLists的请求:

  • 请求

  • 响应



    响应头中多了一个cache-control字段用于指明缓存的类型(public)以及过期时间为60s:



    如果你是使用Postman或者Insomia发送的请求,那么在过期前再次发起相同请求的返回头中会再多出一个Age字段,用于表明该资源当前缓存了多少秒(Hoppscotch我没找到可以在哪里设置,所以下面的截图是来自Insomia,如果有哪位老哥知道的可以教一下):



    同时如果观察日志的话会发现,第二次请求并没有实际执行SQL语句,这也证明了第二次请求的返回来自缓存:



    如果间隔60s以上我们再去发送相同的请求,会发现日志中是这样的:



    可以看到缓存已经失效了,此时需要重新向数据库查询返回数据,并将这次请求结果缓存起来。

验证2: 单个Action覆盖全局配置

我们还是使用这个接口,但是修改一下属性:

  • TodoListController.cs
[HttpGet]
[ResponseCache(Duration = 120)]
public async Task<ApiResponse<List<TodoListBriefDto>>> Get()
{
return ApiResponse<List<TodoListBriefDto>>.Success(await _mediator.Send(new GetTodosQuery()));
}

重新启动Api项目,第一次执行获取TodoLists的请求,请求和验证1相同,我们来看响应中的变化:

  • 响应



    可以看到失效时间已经变为120s了,其他不再一一验证。

使用Marvin.Cache.Headers实现更多缓存功能

在缓存中还有一个问题是,如果判断缓存的数据内容已经变化,就需要去获取最新的响应而不是直接从缓存中取值。这是借助缓存校验来完成的,而常使用的方式是通过Etag实现。示意的过程如下:

如果首次请求资源,API会在响应头中添加EtagLast-Modified字段:

当客户端再次请求资源时,由于缓存自身是不知道资源有没有被修改,所以缓存会携带If-None-Match字段(和客户端收到的Etag值相等)和If-Modified-Since字段(和客户端收到的Last-Modified值相等)到API端,如果校验发现资源没有发生修改,那么API端无需重新获取资源,直接返回304字段(NotModifed)给缓存,缓存给客户端返回值。如果校验发现资源发生了修改,那么API将会返回新的结果。

我们给Api项目添加Nuget包Marvin.Cache.Headers,来实现此功能。

首先向Program中添加服务以及引入中间件:

  • Program.cs
builder.Services.AddResponseCaching();
builder.Services.AddHttpCacheHeaders(
expirationOptions =>
{
expirationOptions.MaxAge = 180;
expirationOptions.CacheLocation = CacheLocation.Private;
},
validateOptions =>
{
validateOptions.MustRevalidate = true;
});
// 省略其他...
app.UseResponseCaching();
app.UseHttpCacheHeaders();

同时我们需要移除之前添加的ResponseCache属性,因为新引入的库已经帮我们完成了,当然我们也可以通过以下方式覆盖全局配置:

[HttpCacheExpiration(CacheLocation = CacheLocation.Public, MaxAge = 60)]
[HttpCacheValidation(MustRevalidate = false)]

覆盖规则和框架内置的规则是一致的,我不会继续演示。

验证3: 缓存校验

请求仍然是获取所有的TodoLists

  • 响应

    我们暂时只关注响应头:



    如果在缓存失效前我们添加了一个新的TodoList,在请求头中添加If-None-Match=53154EEFAE230D733827DBDE49B42AF9再执行获取请求:



    可以看到在失效时间到期之内,Etag值已经发生了变化,校验表明资源已经改变,需要重新获取。

    如果我们再次获取相同的资源,会得到304返回:

一点扩展

但是如果我们仔细观察和思考就会发现,框架在实现缓存校验上存在两个问题:

  1. If-None-Match头字段是我们手动添加模拟的,这本应该由缓存中间件来完成;
  2. 在响应304的情况下,实际上是没有返回响应体的,即缓存中未修改的资源没有返回;

这两个问题是由框架内建的ResponseCaching库导致的,可以认为它没有正确地实现缓存校验机制。为此我们有一些替代方案可供参考:

Varnish

Apache Traffic Server

Squid

当然使用专门的CDN来做缓存也是可以的。

总结

在本文中我们主要演示了如何借助框架的缓存机制来实现请求资源的缓存,尽管在缓存校验的实现上,官方提供的库目前来看并没有能很好地完成功能以外,对于我们基本的使用场景来说已经够用了。下一篇文章我们来看怎么实现接口的限流。

参考资料

  1. Varnish
  2. Apache Traffic Server
  3. Squid

使用.NET 6开发TodoList应用(22)——实现缓存的更多相关文章

  1. 使用.NET 6开发TodoList应用(3)——引入第三方日志库

    需求 在我们项目开发的过程中,使用.NET 6自带的日志系统有时是不能满足实际需求的,比如有的时候我们需要将日志输出到第三方平台上,最典型的应用就是在各种云平台上,为了集中管理日志和查询日志,通常会选 ...

  2. 使用.NET 6开发TodoList应用(1)——系列背景

    前言 想到要写这样一个系列博客,初衷有两个:一是希望通过一个实践项目,将.NET 6 WebAPI开发的基础知识串联起来,帮助那些想要入门.NET 6服务端开发的朋友们快速上手,对使用.NET 6开发 ...

  3. 使用.NET 6开发TodoList应用(2)——项目结构搭建

    为了不影响阅读的体验,我把系列导航放到文章最后了,有需要的小伙伴可以直接通过导航跳转到对应的文章 : P TodoList需求简介 首先明确一下我们即将开发的这个TodoList应用都需要完成什么功能 ...

  4. 使用.NET 6开发TodoList应用(4)——引入数据存储

    需求 作为后端CRUD程序员(bushi,数据存储是开发后端服务一个非常重要的组件.对我们的TodoList项目来说,自然也需要配置数据存储.目前的需求很简单: 需要能持久化TodoList对象并对其 ...

  5. 使用.NET 6开发TodoList应用(5)——领域实体创建

    需求 上一篇文章中我们完成了数据存储服务的接入,从这一篇开始将正式进入业务逻辑部分的开发. 首先要定义和解决的问题是,根据TodoList项目的需求,我们应该设计怎样的数据实体,如何去进行操作? 长文 ...

  6. 使用.NET 6开发TodoList应用(5.1)——实现Repository模式

    需求 经常写CRUD程序的小伙伴们可能都经历过定义很多Repository接口,分别做对应的实现,依赖注入并使用的场景.有的时候会发现,很多分散的XXXXRepository的逻辑都是基本一致的,于是 ...

  7. 使用.NET 6开发TodoList应用(6)——使用MediatR实现POST请求

    需求 需求很简单:如何创建新的TodoList和TodoItem并持久化. 初学者按照教程去实现的话,应该分成以下几步:创建Controller并实现POST方法:实用传入的请求参数new一个数据库实 ...

  8. 使用.NET 6开发TodoList应用文章索引

    系列导航 使用.NET 6开发TodoList应用(1)--系列背景 使用.NET 6开发TodoList应用(2)--项目结构搭建 使用.NET 6开发TodoList应用(3)--引入第三方日志 ...

  9. iOS移动开发周报-第22期

    iOS移动开发周报-第22期 [摘要]:本期iOS移动开发周报带来如下内容:苹果股价创新高,iOS8自动调整UITableView布局,Swift学习心得等. 新闻 <苹果股价创新高 市值全球第 ...

随机推荐

  1. MyBatis一对多映射简单查询案例(嵌套结果)

    一.案例描述 书本类别表和书本信息表,查询书本类别表中的某一记录,连带查询出所有该类别书本的信息. 二.数据库表格 书本类别表(booktypeid,booktypename) 书本信息表(booki ...

  2. [MySQL实战-Mysql基础篇]-mysql的日志

    参考文章: https://www.cnblogs.com/f-ck-need-u/archive/2018/05/08/9010872.html https://dev.mysql.com/doc/ ...

  3. scanf("%c\n",&a)和scanf("%c",&a)区别

    scanf("%c",&a); 当输入字符的时候,我们按下任意字符 + 回车的时候,回车没有被当作为分隔符,而是作为一个转义字符与输入的字符一起保存在缓存区.第一次scan ...

  4. 深入.NET框架与面向对象的回顾

    .NET DOTNET DNET 点NET(.NET框架支持跨语言开发.如C#,VB .NET ,C++.NET,F# ,lronRuby,Others) 任何人,在任何地方,使用任何终端设备,都能访 ...

  5. vue3 到底哪里好?看这一篇就够了

    之前写的关于 vue3 的文章,好多人吐槽:这些API每次使用都要引入一遍,感觉有点麻烦. 今天我们就来看看 vue3 相比 vue2 的优点有些啥? 为啥有些人说:自从写了 ts vue3 再也回不 ...

  6. 学习笔记--html篇(1)

    html学习--1 href学习 href="javascript:void(0)" 阻止页面跳转类似于javascript:#,url无变化(死链接,返回undefined) h ...

  7. [BUUCTF]REVERSE——[ACTF新生赛2020]Oruga

    [ACTF新生赛2020]Oruga 附件 步骤: 例行检查,64位程序,无壳 64位ida载入,检索字符串,根据提示来到关键函数 14行~18行就是让字符串的前5位是 actf{ ,sub_78A( ...

  8. CF1492A Three swimmers 题解

    Update \(\texttt{2021.3.9}\) 修改了题解中的错别字. \(\texttt{2021.12.16}\) 修改了一个没括回的区间. Content 有 \(3\) 个游泳者,同 ...

  9. Linux使用docker安装zimg图片服务器

    官方地址:http://zimg.buaa.us/ 配置文件 zimg.lua --zimg server config --server config --是否后台运行 is_daemon = 1 ...

  10. c++11之函数参数包展开

    1.关于 本文略带总结性,参考:泛化之美--C++11可变模版参数的妙用 参数包展开方式有两种: 递归展开 和 逗号表达式展开. 本文代码并非全部来自参考文章,自己做了注释和修改.请以原文为准 2. ...