ASP.NET Core中的缓存[1]:如何在一个ASP.NET Core应用中使用缓存

.NET Core针对缓存提供了很好的支持 ,我们不仅可以选择将数据缓存在应用进程自身的内存中,还可以采用分布式的形式将缓存数据存储在一个“中心数据库”中。对于分布式缓存,.NET Core提供了针对Redis和SQL Server的原生支持。除了这个独立的缓存系统之外,ASP.NET Core还借助一个中间件实现了“响应缓存”,它会按照HTTP缓存规范对整个响应实施缓存。不过按照惯例,在对缓存进行系统介绍之前,我们还是先通过一些简单的实例演示感知一下如果在一个ASP.NET Core应用中如何使用缓存。

目录
一、将数据缓存在内存中
二、基于Redis的分布式缓存
三、基于SQL Server的分布式缓存
四、缓存整个HTTP响应

一、将数据缓存在内存中

与针对数据库和远程服务调用这种IO操作来说,应用针对内存的访问性能将提供不止一个数量级的提升,所以将数据直接缓存在应用进程的内容中自然具有最佳的性能优势。与基于内存的缓存相关的应用编程接口定义在NuGet包“Microsoft.Extensions.Caching.Memory”中,具体的缓存实现在一个名为MemoryCache的服务对象中,后者是我们对所有实现了IMemoryCache接口的所有类型以及对应对象的统称。由于是将缓存对象直接置于内存之中,中间并不涉及持久化存储的问题,自然也就无需考虑针对缓存对象的序列化问题,所以这种内存模式支持任意类型的缓存对象。

针对缓存的操作不外乎对缓存数据的存与取,这两个基本的操作都由上面介绍的这个MemoryCache对象来完成。如果我们在一个ASP.NET Core应用对MemoryCache服务在启动时做了注册,我们就可以在任何地方获取该服务对象设置和获取缓存数据,所以针对缓存的编程是非常简单的。

   1: public class Program
   2: {
   3:     static void Main()
   4:     {        
   5:         new WebHostBuilder()
   6:             .UseKestrel()
   7:             .ConfigureServices(svcs => svcs.AddMemoryCache())
   8:             .Configure(app => app.Run(async context =>
   9:                 {
  10:                     IMemoryCache cache = context.RequestServices.GetRequiredService<IMemoryCache>();
  11:                     DateTime currentTime;
  12:                     if (!cache.TryGetValue<DateTime>("CurrentTime",out currentTime))
  13:                     {
  14:                         cache.Set(  15:                     }
  16:                     await context.Response.WriteAsync($"{currentTime}({DateTime.Now})");
  17:                 }))
  18:             .Build()
  19:             .Run();        
  20:     }
  21: }

在上面这个演示程序中,我们在WebHostBuilder的ConfigureServices方法中通过调用ServiceCollection的扩展方法AddMemoryCache完成了针对MemoryCache的服务注册。在WebHostBuilder的Configure方法中,我们通过调用ApplicationBuilder的Run方法注册了一个中间件对请求做了简单的响应。我们先从当前HttpContext中得到对应的ServiceProvider,并利用后者得到MemoryCache对象。我们接下来调用MemoryCache的Set方法将当前时间缓存起来(如果尚未缓存),并指定一个唯一的Key(“CurrentTime”)。通过指定响应的Key,我们可以调用另一个名为TryGetValue<T>的方法获取缓存的对象。我们最终写入的响应内容实际上是缓存的时候和当前实施的时间。由于缓存的是当前时间,所以当我们通过浏览器访问该应用的时候,显示的时间在缓存过期之前总是不变的

1

虽然基于内存的缓存具有最高的性能,但是由于它实际上是将缓存数据存在承载ASP.NET Core应用的Web服务上,对于部署在集群式服务器中的应用会出现缓存数据不一致的情况。对于这种部署场景,我们需要将数据缓存在某一个独立的存储中心,以便让所有的Web服务器共享同一份缓存数据,我们将这种缓存形式称为“分布式缓存”。ASP.NET Core为分布式缓存提供了两种原生的存储形式,一种是基于NoSQL的Redis数据库,另一种则是微软自家关系型数据库SQL Server。

二、基于Redis的分布式缓存

Redis数目前较为流行NoSQL数据库,很多的编程平台都将它作为分布式缓存的首选,接下来我们来演示如何在一个ASP.NET Core应用中如何采用基于Redis的分布式缓存。考虑到一些人可能还没有体验过Redis,所以我们先来简单介绍一下如何安装Redis。Redis最简单的安装方式就是采用Chocolatey(https://chocolatey.org/) 命令行,后者是Windows平台下一款优秀的软件包管理工具(类似于NPM)。

2: iwr https://chocolatey.org/install.ps1 -UseBasicParsing | iex
   4: CMD.exe:
 && SET "PATH=%PATH%;%ALLUSERSPROFILE%\chocolatey\bin"

我们既可以采用PowerShell (要求版本在V3以上)命令行或者普通CMD.exe命令行来安装Chocolatey ,具体的命令如上所示。在确保Chocolatey 被本地正常安装情况下,我们可以执行执行如下的命令安装或者升级64位的Redis。

2: C:\>choco upgrade redis-64

Redis服务器的启动也很简单,我们只需要以命令行的形式执行redis-server命令即可。如果在执行该命名之后看到如下图所示的输出,则表示本地的Redis服务器被正常启动,输出的结果会指定服务器采用的网络监听端口。

2

接下来我们会对上面演示的实例进行简单的修改,将基于内存的本地缓存切换到针对Redis数据库的分布式缓存。针对Redis的分布式缓存实现在NuGet包“Microsoft.Extensions.Caching.Redis”之中,所以我们需要确保该NuGet包被正常安装。不论采用Redis、SQL Server还是其他的分布式存储方式,针对分布式缓存的操作都实现在DistributedCache这个服务对象向,该服务对应的接口为IDistributedCache。

7:             .ConfigureServices(svcs => svcs.AddDistributedRedisCache(options =>
   9:                     options.Configuration    = "localhost";
  11:                 }))
  13:                 {
  15:                     string currentTime = await cache.GetStringAsync("CurrentTime");
  17:                     {
  19:                         await cache.SetAsync(  20:                     }
  22:                 }))
  23:             .Build()
  24:             .Run();
  25:     }
  26: }

从上面的代码片段可以看出,针对分布式缓存和内存缓存在总体编程模式上是一致的,我们需要先注册针对DistributedCache的服务注册,但是利用依赖注入机制提供该服务对象来进行缓存数据的设置和缓存。我们调用IServiceCollection的另一个扩展方法AddDistributedRedisCache注册DistributedCache服务,在调用这个方法的时候借助于RedisCacheOptions这个对象的Configuration和InstanceName属性设置Redis数据库的服务器和实例名称。由于采用的是本地的Redis服务器,所以我们将前者设置为“localhost”。其实Redis数据库并没有所为的实例的概念,RedisCacheOptions的InstanceName属性的目的在于当多个应用共享同一个Redis数据库的时候,缓存数据可以利用它来区分,当缓存数据被保存到Redis数据库中的时候,对应的Key会以它为前缀。修改后的应用启动后(确保Redis服务器被正常启动),如果我们利用浏览器来访问它,依然会得到与前面类似的输出。

对于基于内存的本地缓存来说,我们可以将任何类型的数据置于缓存之中,但是对于分布式缓存来说,由于涉及到网络传输甚至是持久化存储,放到缓存中的数据类型只能是字节数组,所以我们需要自行负责对缓存对象的序列化和反序列化工作。如上面的代码片段所示,我们先将表示当前时间的DateTime对象转换成字符串,然后采用UTF-8编码进一步转换成字节数组,最终调用DistributedCache的SetAsync方法将后者缓存起来。实际上我们也可以直接调用另一个扩展方法SetStringAsync,它会负责将字符串编码为字节数组。在获取缓存的时候,我们调用的是DistributedCache的GetStringAsync方法,它会将字节数组转换成字符串。

缓存数据在Redis数据库中是以散列(Hash)的形式存放的,对应的Key会将设置的InstanceName作为前缀(如果进行了设置)。为了查看究竟存放了哪些数据在Redis数据库中,我们可以按照如图3所示的形式执行Redis命名来获取存储的数据。从下图呈现的输出结果我们不难看出,存入的不仅仅包括我们指定的缓存数据(Sub-Key为“data”)之外,还包括其他两组针对该缓存条目的描述信息,对应的Sub-Key分别为“absexp”和“sldexp”,表示缓存的绝对过期时间(Absolute Expiration Time)和滑动过期时间(Slidding Expiration Time)。

3

三、基于SQL Server的分布式缓存

除了使用Redis这种主流的NoSQL数据库来支持分布式缓存,微软在设计分布式缓存时也没有忘记自家的关系型数据库采用SQL Server。针对SQL Server的分布式缓存实现在“Microsoft.Extensions.Caching.SqlServer”这个NuGet包中,我们先得确保该NuGet包被正常装到演示的应用中。

所谓的针对SQL Server的分布式缓存,实际上就是将标识缓存数据的字节数组存放在SQL Server数据库中某个具有固定结构的数据表中,因为我们得先来创建这么一个缓存表,该表可以借助一个名为sql-cache 的工具来创建。在执行sql-cache 工具创建缓存表之前,我们需要在project.json文件中按照如下的形式为这个工具添加相应的NuGet包“Microsoft.Extensions.Caching.SqlConfig.Tools”。

2:   …
   4:     "Microsoft.Extensions.Caching.SqlConfig.Tools": "1.1.0-preview4-final"
   6: }

当针对上述这个NuGet包复原(Restore)之后,我们可以执行“dotnet sql-cache create”命令来创建,至于这个执行这个命令应该指定怎样的参数,我们可以按照如下的形式通过执行“dotnet sql-cache create --help”命令来查看。从下图可以看出,该命名需要指定三个参数,它们分别表示缓存数据库的链接字符串、缓存表的Schema和名称。

4

接下来我们只需要在演示应用所在的项目根目录(project.json文件所在的目录)下执行dotnet sql-cache create就可以在指定的数据库创建缓存表了。对于我们演示的实例来说,我们按照下图所示的方式执行这dotnet sql-cache create命令行在本机一个名为demodb的数据库中创建了一个名为AspnetCache的缓存表,该表采用dbo作为Schema。

5

在所有的准备工作完成之后,我们只需要对上面的程序做如下的修改即可将针对Redis数据库的缓存切换到针对SQL Server数据库的缓存。由于采用的同样是分布式缓存,所以针对缓存数据的设置和提取的代码不用做任何改变,我们需要修改的地方仅仅是服务注册部分。如下面的代码片段所示,我们在WebHostBuilder的ConfigureServices方法中调用IServiceCollection的扩展方法AddDistributedSqlServerCache完成了对应的服务注册。在调用这个方法的时候,我们通过设置SqlServerCacheOptions对象的三个属性的方式指定了缓存数据库的链接字符串和缓存表的Schema和名称。

8:             {
  10:                 options.SchemaName         = "dbo";
  12:             }))
  14:                 {
  17:                       18:                     {
  20:                         await cache.SetAsync(  21:                     }
  24:             .Build()
  26:     }
  27: }

如果想看看最终存入SQL Server数据库中的究竟包含哪些缓存数据,我们只需要直接在所在数据库中查看对应的缓存表了。对于演示实例缓存的数据,它会以下图所示的形式保存在我们创建的缓存表(AspnetCache)中,与基于Redis的缓存类似,与指定缓存数据的值一并存储的还包括缓存的过期信息。

6

四、缓存整个HTTP响应

上面演示的两种缓存都要求我们利用注册的服务对象以手工的方式存储和提取具体的缓存数据,而接下来我们演示的缓存则不再基于某个具体的缓存数据,而是将服务端最终生成的响应主体内容予以缓存,我们将这种缓存形式称为响应缓存(Response Caching)。标准的HTTP规范,不论是HTTP 1.0+还是HTTP 1.1,都会缓存做了详细的规定,这是响应规范的理论机制和指导思想。我们将在后续内容中详细介绍HTTP缓存,在这之前我们先通过一个简单的实例来演示一下整个响应内容是如何借助一个名为ResponseCachingMiddleware中间件被缓存起来的。该中间件由“Microsoft.AspNetCore.ResponseCaching”这个NuGet包提供。

通过同样是采用基于时间的缓存场景,为此我们编写了如下这个简单的程序。我们在WebHostBuilder的ConfigureServices方法中调用了IServiceCollection接口的扩展方法AddResponseCaching注册了中间件ResponseCachingMiddleware依赖的所有的服务,而这个中间件的注册则通过调用IApplicationBuilder接口的扩展方法UseResponseCaching完成。

8:             .Configure(app => app
  10:                 .Run(async context => {
  12:                         {
  14:                             MaxAge = TimeSpan.FromSeconds(3600)
  16:                     
  18:                     bool isUtc = string.Equals(utc,1)">"true",StringComparison.OrdinalIgnoreCase);
  20:                 }))
  22:             .Run();    
  24: }

对于最终实现的请求处理逻辑来说,我们仅仅是为响应添加了一个Cache-Control报头,并将它的值设置为“public,max-age=3600”(public表示缓存的是可以被所有用户共享的公共数据,而max-age则表示过去时限,单位为秒)。真正写入响应的主体内容就是当前时间,不给过我们会根据请求的查询字符串“utc”决定采用普通时间还是UTC时间。

要证明整个响应的内容是否被被缓存起来,我们只需要验证在缓存过期之前具有相同路径的多个请求对应的响应是否具有相同的主体内容,为此我们采用Fiddler来生发送的请求并拦截响应的内容。如下所示的两组请求和响应是在不同时间发送的,我们可以看出响应的内容是完全一致的。由于请求发送的时间不同,所以返回的缓存副本的“年龄”(对应于响应报头Age)也是不同的。

2: User-Agent: Fiddler
   4:  
   6: Date: Sun,12 Feb 2017 13:02:23 GMT
   8: Server: Kestrel
  10: Age: 82
  12: 2/12/2017 1:02:23 PM
  14:  
  17: Host: localhost:5000
  19: HTTP/1  20: Date: Sun,1)">  21: Content-Length: 20
  23: Cache-Control: public,1)">  24: Age: 85
  26: 2/12/2017 1:02:23 PM

上面这个两个请求的URL并没有携带“utc”查询字符串,所以返回的是一个非UTC时间,接下来我们采用相同的方式生成一个试图返回UTC时间的请求。从下面给出的请求和响应的内容我们可以看出,虽然请求携带了查询字符串“utc=true”,但是返回的依然是之前缓存的时间。由于此可见,ResponseCachingMiddleware中间件在默认情况下是针对请求的路径对响应实施缓存的,它会忽略请求URL携带的查询字符串,这显然不是我们希望看到的结果。

12: 2/12/2017 1:02:23 PM

按照REST的原则,URL是网路资源的标识,但是资源的表现形式(Representation)会由一些参数来决定,这些参数可以体现为查询字符串,也可以体现为一些请求报头,比如Language报头决定资源的描述语言,Content-Encoding报头决定资源采用的编码方式。因此针对响应的缓存不应该只考虑请求的路径,还应该综合考虑这些参数。

对于演示的这个实例来说,我们希望将查询字符串“utc”纳入缓存考虑的范畴,这可以利用一个名为ResponseCachingFeature的特性来完成,该特性对应的接口为IResponseCachingFeature。如下面的代码片段所示,在将当前时间写入响应之后,我们得到这个特性并设置了它的VaryByQueryKeys属性,该属性包含一组决定输出缓存的查询字符串名称,我们将查询字符“utc”添加到这个列表中。

12:                     var feature = context.Features.Get<IResponseCachingFeature>();
  14:                 }))
  16:             .Run();    
  18: }

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


在上文中,我介绍了事件驱动型架构的一种简单的实现,并演示了一个完整的事件派发、订阅和处理的流程。这种实现太简单了,百十行代码就展示了一个基本工作原理。然而,要将这样的解决方案运用到实际生产环境,还有很长的路要走。今天,我们就研究一下在事件处理器中,对象生命周期的管理问题。事实上,不仅仅是在事件处理器
上文已经介绍了Identity Service的实现过程。今天我们继续,实现一个简单的Weather API和一个基于Ocelot的API网关。 回顾 《Angular SPA基于Ocelot API网关与IdentityServer4的身份认证与授权(一)》 Weather API Weather
最近我为我自己的应用开发框架Apworks设计了一套案例应用程序,并以Apache 2.0开源,开源地址是:https://github.com/daxnet/apworks-examples,目的是为了让大家更为方便地学习和使用.NET Core、最新的前端开发框架Angular,以及Apwork
HAL(Hypertext Application Language,超文本应用语言)是一种RESTful API的数据格式风格,为RESTful API的设计提供了接口规范,同时也降低了客户端与服务端接口的耦合度。很多当今流行的RESTful API开发框架,包括Spring REST,也都默认支
在前面两篇文章中,我详细介绍了基本事件系统的实现,包括事件派发和订阅、通过事件处理器执行上下文来解决对象生命周期问题,以及一个基于RabbitMQ的事件总线的实现。接下来对于事件驱动型架构的讨论,就需要结合一个实际的架构案例来进行分析。在领域驱动设计的讨论范畴,CQRS架构本身就是事件驱动的,因此,
HAL,全称为Hypertext Application Language,它是一种简单的数据格式,它能以一种简单、统一的形式,在API中引入超链接特性,使得API的可发现性(discoverable)更强,并具有自描述的特点。使用了HAL的API会更容易地被第三方开源库所调用,并且使用起来也很方便
何时使用领域驱动设计?其实当你的应用程序架构设计是面向业务的时候,你已经开始使用领域驱动设计了。领域驱动设计既不是架构风格(Architecture Style),也不是架构模式(Architecture Pattern),它也不是一种软件开发方法论,所以,是否应该使用领域驱动设计,以及什么时候使用
《在ASP.NET Core中使用Apworks快速开发数据服务》一文中,我介绍了如何使用Apworks框架的数据服务来快速构建用于查询和管理数据模型的RESTful API,通过该文的介绍,你会看到,使用Apworks框架开发数据服务是何等简单快捷,提供的功能也非常多,比如对Hypermedia的
在上一讲中,我们已经完成了一个完整的案例,在这个案例中,我们可以通过Angular单页面应用(SPA)进行登录,然后通过后端的Ocelot API网关整合IdentityServer4完成身份认证。在本讲中,我们会讨论在当前这种架构的应用程序中,如何完成用户授权。 回顾 《Angular SPA基于
Keycloak是一个功能强大的开源身份和访问管理系统,提供了一整套解决方案,包括用户认证、单点登录(SSO)、身份联合、用户注册、用户管理、角色映射、多因素认证和访问控制等。它广泛应用于企业和云服务,可以简化和统一不同应用程序和服务的安全管理,支持自托管或云部署,适用于需要安全、灵活且易于扩展的用
3月7日,微软发布了Visual Studio 2017 RTM,与之一起发布的还有.NET Core Runtime 1.1.0以及.NET Core SDK 1.0.0,尽管这些并不是最新版,但也已经从preview版本升级到了正式版。所以,在安装Visual Studio 2017时如果启用了
在上文中,我介绍了如何在Ocelot中使用自定义的中间件来修改下游服务的response body。今天,我们再扩展一下设计,让我们自己设计的中间件变得更为通用,使其能够应用在不同的Route上。比如,我们可以设计一个通用的替换response body的中间件,然后将其应用在多个Route上。 O
不少关注我博客的朋友都知道我在2009年左右开发过一个名为Apworks的企业级应用程序开发框架,旨在为分布式企业系统软件开发提供面向领域驱动(DDD)的框架级别的解决方案,并对多种系统架构风格提供支持。这个框架的开发和维护我坚持了很久,一直到2015年,我都一直在不停地重构这个项目。目前这个项目在
好吧,这个题目我也想了很久,不知道如何用最简单的几个字来概括这篇文章,原本打算取名《Angular单页面应用基于Ocelot API网关与IdentityServer4ʺSP.NET Identity实现身份认证与授权》,然而如你所见,这样的名字实在是太长了。所以,我不得不缩写“单页面应用”几个字
在前面两篇文章中,我介绍了基于IdentityServer4的一个Identity Service的实现,并且实现了一个Weather API和基于Ocelot的API网关,然后实现了通过Ocelot API网关整合Identity Service做身份认证的API请求。今天,我们进入前端开发,设计
Ocelot是ASP.NET Core下的API网关的一种实现,在微服务架构领域发挥了非常重要的作用。本文不会从整个微服务架构的角度来介绍Ocelot,而是介绍一下最近在学习过程中遇到的一个问题,以及如何使用中间件(Middleware)来解决这样的问题。 问题描述 在上文中,我介绍了一种在Angu
在大数据处理和人工智能时代,数据工厂(Data Factory)无疑是一个非常重要的大数据处理平台。市面上也有成熟的相关产品,比如Azure Data Factory,不仅功能强大,而且依托微软的云计算平台Azure,为大数据处理提供了强大的计算能力,让大数据处理变得更为稳定高效。由于工作中我的项目
在上文中,我们讨论了事件处理器中对象生命周期的问题,在进入新的讨论之前,首先让我们总结一下,我们已经实现了哪些内容。下面的类图描述了我们已经实现的组件及其之间的关系,貌似系统已经变得越来越复杂了。其中绿色的部分就是上文中新实现的部分,包括一个简单的Event Store,一个事件处理器执行上下文的接
在之前《在ASP.NET Core中使用Apworks快速开发数据服务》一文的评论部分,.NET大神张善友为我提了个建议,可以使用Compile As a Service的Roslyn为语法解析提供支持。在此非常感激友哥给我的建议,也让我了解了一些Roslyn的知识。使用Roslyn的一个很大的好处
很长一段时间以来,我都在思考如何在ASP.NET Core的框架下,实现一套完整的事件驱动型架构。这个问题看上去有点大,其实主要目标是为了实现一个基于ASP.NET Core的微服务,它能够非常简单地订阅来自于某个渠道的事件消息,并对接收到的消息进行处理,于此同时,它还能够向该渠道发送事件消息,以便