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:     public 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("CurrentTime", currentTime = DateTime.Now);
  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>的方法获取缓存的对象。我们最终写入的响应内容实际上是缓存的时候和当前实施的时间。由于缓存的是当前时间,所以当我们通过浏览器访问该应用的时候,显示的时间在缓存过期之前总是不变的

虽然基于内存的缓存具有最高的性能,但是由于它实际上是将缓存数据存在承载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)。

   1: PowerShell prompt :
   2: iwr https://chocolatey.org/install.ps1 -UseBasicParsing | iex
   3:  
   4: CMD.exe:
   5: @powershell -NoProfile -ExecutionPolicy Bypass -Command "iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))" && SET "PATH=%PATH%;%ALLUSERSPROFILE%\chocolatey\bin"

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

   1: C:\>choco install redis-64
   2: C:\>choco upgrade redis-64

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

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

   1: public class Program
   2: {
   3:     public static void Main()
   4:     {
   5:         new WebHostBuilder()
   6:             .UseKestrel()
   7:             .ConfigureServices(svcs => svcs.AddDistributedRedisCache(options =>
   8:                 {
   9:                     options.Configuration    = "localhost";
  10:                     options.InstanceName     = "Demo";
  11:                 }))
  12:             .Configure(app => app.Run(async context =>
  13:                 {
  14:                     var cache = context.RequestServices.GetRequiredService<IDistributedCache>();
  15:                     string currentTime = await cache.GetStringAsync("CurrentTime");
  16:                     if (null == currentTime)
  17:                     {
  18:                         currentTime = DateTime.Now.ToString();
  19:                         await cache.SetAsync("CurrentTime", Encoding.UTF8.GetBytes(currentTime));
  20:                     }
  21:                     await context.Response.WriteAsync($"{currentTime}({DateTime.Now})");
  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)。

三、基于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”。

   1: {
   2:   …
   3:   "tools": {
   4:     "Microsoft.Extensions.Caching.SqlConfig.Tools": "1.1.0-preview4-final"
   5:   }
   6: }

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

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

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

   1: public class Program
   2: {
   3:     public static void Main()
   4:     {
   5:         new WebHostBuilder()
   6:             .UseKestrel()
   7:             .ConfigureServices(svcs => svcs.AddDistributedSqlServerCache(options =>
   8:             {
   9:                 options.ConnectionString   = "server=.;database=demodb;uid=sa;pwd=password";
  10:                 options.SchemaName         = "dbo";
  11:                 options.TableName          = "AspnetCache";
  12:             }))
  13:             .Configure(app => app.Run(async context =>
  14:                 {
  15:                     var cache = context.RequestServices.GetRequiredService<IDistributedCache>();
  16:                     string currentTime = await cache.GetStringAsync("CurrentTime");
  17:                     if (null == currentTime)
  18:                     {
  19:                         currentTime = DateTime.Now.ToString();
  20:                         await cache.SetAsync("CurrentTime", Encoding.UTF8.GetBytes(currentTime));
  21:                     }
  22:                     await context.Response.WriteAsync($"{currentTime}({DateTime.Now})");
  23:                 }))
  24:             .Build()
  25:             .Run();
  26:     }
  27: }

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

四、缓存整个HTTP响应

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

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

   1: public class Program
   2: {
   3:     public static void Main()
   4:     {
   5:         new WebHostBuilder()
   6:             .UseKestrel()
   7:             .ConfigureServices(svcs=>svcs.AddResponseCaching())
   8:             .Configure(app => app
   9:                 .UseResponseCaching()
  10:                 .Run(async context => {
  11:                     context.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
  12:                         {
  13:                             Public = true,
  14:                             MaxAge = TimeSpan.FromSeconds(3600)
  15:                         };
  16:                     
  17:                     string utc = context.Request.Query["utc"].FirstOrDefault()??"";
  18:                     bool isUtc = string.Equals(utc, "true", StringComparison.OrdinalIgnoreCase);
  19:                     await context.Response.WriteAsync(isUtc? DateTime.UtcNow.ToString(): DateTime.UtcNow.ToString());
  20:                 }))
  21:             .Build()
  22:             .Run();    
  23:     }
  24: }

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

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

   1: GET http://localhost:5000/ HTTP/1.1
   2: User-Agent: Fiddler
   3: Host: localhost:5000
   4:  
   5: HTTP/1.1 200 OK
   6: Date: Sun, 12 Feb 2017 13:02:23 GMT
   7: Content-Length: 20
   8: Server: Kestrel
   9: Cache-Control: public, max-age=3600
  10: Age: 
  11:  
  12: 2/12/2017 1:02:23 PM
  13:  
  14:  
  15: GET http://localhost:5000/ HTTP/1.1
  16: User-Agent: Fiddler
  17: Host: localhost:5000
  18:  
  19: HTTP/1.1 200 OK
  20: Date: Sun, 12 Feb 2017 13:02:23 GMT
  21: Content-Length: 20
  22: Server: Kestrel
  23: Cache-Control: public, max-age=3600
  24: Age: 
  25:  
  26: 2/12/2017 1:02:23 PM

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

   1: GET http://localhost:5000/?utc=true HTTP/1.1
   2: User-Agent: Fiddler
   3: Host: localhost:5000
   4:  
   5: HTTP/1.1 200 OK
   6: Date: Sun, 12 Feb 2017 13:02:23 GMT
   7: Content-Length: 20
   8: Server: Kestrel
   9: Cache-Control: public, max-age=3600
  10: Age: 474
  11:  
  12: 2/12/2017 1:02:23 PM

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

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

   1: public class Program
   2: {
   3:     public static void Main()
   4:     {
   5:         new WebHostBuilder()
   6:             .UseKestrel()
   7:             .ConfigureServices(svcs=>svcs.AddResponseCaching())
   8:             .Configure(app => app
   9:                 .UseResponseCaching()
  10:                 .Run(async context => {
  11:                     …
  12:                     var feature = context.Features.Get<IResponseCachingFeature>();
  13:                     feature.VaryByQueryKeys = new string[] { "utc" };                     
  14:                 }))
  15:             .Build()
  16:             .Run();    
  17:     }
  18: }
作者:蒋金楠 
微信公众账号:大内老A
微博:www.weibo.com/artech

NET Core应用中使用缓存的更多相关文章

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

    .NET Core针对缓存提供了很好的支持 ,我们不仅可以选择将数据缓存在应用进程自身的内存中,还可以采用分布式的形式将缓存数据存储在一个“中心数据库”中.对于分布式缓存,.NET Core提供了针对 ...

  2. ASP.NET Core 中的缓存

    目录 缓存的基本概念 缓存原理 缓存设计 分布式缓存 Memcache 与 Redis 的比较 缓存穿透,缓存击穿,缓存雪崩解决方案 数据一致性 使用内置 MemoryCache 使用分布式缓存 Re ...

  3. .NET Core应用中使用分布式缓存及内存缓存

    .NET Core针对缓存提供了很好的支持 ,我们不仅可以选择将数据缓存在应用进程自身的内存中,还可以采用分布式的形式将缓存数据存储在一个“中心数据库”中.对于分布式缓存,.NET Core提供了针对 ...

  4. .NET CORE 中的缓存使用

    Net Framewoke的缓存 1.1 System.Web.Caching System.Web.Caching应该是我们最熟悉的缓存类库了,做ASP.NET开发时用到缓存基本都是使用的这个缓存组 ...

  5. NET Core静态文件的缓存方式

    NET Core静态文件的缓存方式 阅读目录 一.前言 二.StaticFileMiddleware 三.ASP.NET Core与CDN? 四.写在最后 回到目录 一.前言 我们在优化Web服务的时 ...

  6. .Net Core 项目中的包引用探索(使用VSCode)

    本文组织有点乱,先说结论吧: 1 在 project.json 文件中声明包引用. 而不是像以前那样可以直接引用 dll. 2 使用 dotnet restore 命令后,nuget 会把声明的依赖项 ...

  7. 拥抱.NET Core系列:MemoryCache 缓存选项

    在上一篇 "拥抱.NET Core系列:MemoryCache 缓存过期" 中我们详细的了解了缓存过期相关的内容,今天我们来介绍一下 MSCache 中的 Options,由此来介 ...

  8. 拥抱.NET Core系列:MemoryCache 缓存域

    在上一篇“<拥抱.NET Core系列:MemoryCache 缓存选项>”我们介绍了一些 MSCache 的机制,今天我们来介绍一下 MSCache 中的缓存域. MSCache项目 M ...

  9. 007.Adding a view to an ASP.NET Core MVC app -- 【在asp.net core mvc中添加视图】

    Adding a view to an ASP.NET Core MVC app 在asp.net core mvc中添加视图 2017-3-4 7 分钟阅读时长 本文内容 1.Changing vi ...

随机推荐

  1. [Swift]LeetCode768. 最多能完成排序的块 II | Max Chunks To Make Sorted II

    This question is the same as "Max Chunks to Make Sorted" except the integers of the given ...

  2. 关于Python pandas模块输出每行中间省略号问题

    关于Python数据分析中pandas模块在输出的时候,每行的中间会有省略号出现,和行与行中间的省略号....问题,其他的站点(百度)中的大部分都是瞎写,根本就是复制黏贴以前的版本,你要想知道其他问题 ...

  3. socket编程: TypeError: must be bytes or buffer, not str

    先看一段代码 #!/usr/bin/env python3 from socket import * serverName = "10.10.10.132" serverPort ...

  4. WebSocket(3)---实现一对一聊天功能

    实现一对一聊天功能 功能介绍:实现A和B单独聊天功能,即A发消息给B只能B接收,同样B向A发消息只能A接收. 本篇博客是在上一遍基础上搭建,上一篇博客地址:[WebSocket]---实现游戏公告功能 ...

  5. Lucene 05 - 使用Lucene的Java API实现分页查询

    目录 1 Lucene的分页查询 2 代码示例 3 分页查询结果 1 Lucene的分页查询 搜索内容过多时, 需要考虑分页显示, 像这样: 说明: Lucene的分页查询是在内存中实现的. 2 代码 ...

  6. EF架构~Dapper.Contrib不能将Linq翻译好发到数据库,所以请不要用它

    回到目录 对于Dapper是一个轻量级的数据访问框架,而需要使用者去自己做SQL,它,只是一个数据访问者! 对些,Dapper推出了Contrib扩展包,它可以友好的让开发人员使用linq,而不需要写 ...

  7. 如何比较版本号--Python实现

    需求 在写一个程序Django项目的setup程序(初始化环境,比如设置PIP源,安装该项目依赖的各种模块等操作)遇到一个系统当前模块版本和项目所需版本的比较然后给出建议是忽略还是升级.我的要求是不仅 ...

  8. C#2.0之细说泛型

    C#2的头号亮点 : 泛型 在C#1中,Arraylist总是会给人带来困扰,因为它的参数类型是Object,这就让开发者无法把握集合中都有哪些类型的数据.如果对string类型的数据进行算术操作那自 ...

  9. 自定义微信小程序导航(兼容各种手机)

    详细代码请见github,请点击地址,其中有原生小程序的实现,也有wepy版本的实现 了解小程序默认导航 如上图所示,微信导航分为两部分,第一个部分为statusBarHeight,刘海屏手机(iPh ...

  10. shell之最常用的服务脚本

    任务需求:以最简单的方式管理 /usr/local/php7/sbin/php-fpm -c /usr/local/php7/etc/php.ini 这条命令 包括启动停止重启 使用技术:shell脚 ...