ASP.NET Core中如何使用缓存

Posted 断流

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ASP.NET Core中如何使用缓存相关的知识,希望对你有一定的参考价值。

相关知识预备

序列化

序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式(json/xml等)的过程。

在序列化期间,对象将其当前状态写入到临时或持久性存储区,以后就可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。

通常的序列化过程有:对象到 JSON 字符串,对象到 XML 字符串,对象到字节数组。

分布式

分指拆分、布指部署,即将一个大的系统或服务拆分开来进行分别部署运行的模式。

Redis

Redis 是目前较为流行的 NoSQL 数据库。Redis提供了键过期功能,也提供了灵活的键淘汰策略,所以,现在Redis用在缓存的场合非常多,也很多编程平台都将其作为分布式缓存的首选。

如何安装 Redis :

方式一:Chocolatey 是 Windows 平台下一款优秀的软件包管理工具(类似于 NPM ),该工具安装教程:Chocolatey Software | Installing Chocolatey。最简单的安装Redis的方法就是使用该工具进行安装。

方式二:windows下安装Redis并部署成服务 - 丁国丰 - 博客园 (cnblogs.com)

缓存

缓存是一种对热点数据或信息的存放方式,通常指存放于内存中。

因为缓存位于内存中,而内存的读取速度要比磁盘快的多。因此能够很快响应用户请求。特别针对一些热点数据,优势尤为明显。

缓存(Caching)是最常用于提高应用程序的性能的一种手段。合理的利用缓存不仅能够提升网站访问速度,还能大大降低数据库的压力。

用户对某些数据的请求量特别大,例如新闻、商品等热点信息,之前模式下都是从数据库直接获取信息,易增加数据库IO压力,久而久之,数据库成为了整个系统的瓶颈。而若将热点数据通过缓存技术来获取,而不再直接访问数据库,就可以减少数据库IO压力。

缓存的适用场景

  • 对于数据实时性要求不高

    对于一些经常访问但是很少改变的数据,读明显多于写,使用缓存就很有必要。比如一些网站配置项。

  • 对于性能要求高(请求的响应时间越短越好)

    比如一些秒杀活动场景。

ASP.NET Core 与 缓存

ASP.NET Core 提供了两个独立的缓存框架,一个是针对本地内存的缓存,另一个是针对分布式存储的缓存

针对本地内存的缓存,可以在不经过序列化的情况下,直接将对象存储在当前应用程序进程的内存中。

分布式缓存则需要将对象序列化成字节数组,并存储到一个独立的中心数据库(Redis/Sql Server)中。

此外,还借助一个中间件,实现了响应缓存,即按照HTTP缓存规范,对整个响应内容实施缓存。

缓存的有效期和刷新方面,有必要先了解两种针对时间的过期策略

  • 绝对时间过期策略:不论缓存对象最近使用的频率如何,对应的 ICacheEntry 对象总是在指定的时间点之后过期。注意:当 AbsoluteExpiration 和 AbsoluteExpirationRelativeToNow 都设置值的时候,绝对过期时间取距离当前时间近的那个设置。
  • 滑动过期:如果在指定的时间内没有读取过该缓存条目,缓存将会过期。反之,针对缓存的每一次使用都会将过期时间延长,指定的这个时间段(SlidingExpiration)就是后延的时长。

如果同时设置了绝对过期时间为1小时后,且滑动过期时间为5分钟,则新添加的缓存条目的过期时间为:5分钟。即多久后过期采用的算法为:Min(AbsoluteExpiration - Now , SlidingExpiration)

缓存数据通常采用字典类型的存储结构,并通过提供的key来定位目标缓存条目。

1、将数据缓存在内存中

即将数据缓存在当前应用程序进程的内存中,这种针对本地内存的缓存,可以在不经过序列化的情况下,直接将对象存储在当前应用程序进程的内存中。这种方法可以获得最高的性能优势。

代码中使用步骤示例:

添加 Nuget 引用:Microsoft.Extensions.Caching.Memory

在 startup.cs 服务配置里面加上:services.AddMemoryCache();

上面将服务添加到依赖注入容器后,就可以在程序中使用了:

    //HomeController
    private readonly IMemoryCache memoryCache;//本地内存缓存

    public HomeController(IMemoryCache cache)
    
        this.memoryCache = cache;
    
    public string TestCache()
    
        if (!memoryCache.TryGetValue<DateTime>("CurrentTime",out var time))
        
            memoryCache.Set("CurrentTime", time = DateTime.Now);
            //如果想设置过期时间(绝对过期时间)。
            //memoryCache.Set("UpdateTime", time = DateTime.Now, DateTime.Now.AddSeconds(10));
            //如果想设置过期时间。滑动时间
            //memoryCache.Set("",time=DateTime.Now,new MemoryCacheEntryOptions()  SlidingExpiration=new TimeSpan(0,0,10));
        
        return $"缓存时间:time(,当前时间:DateTime.Now)";
    

注意:这种内存中的缓存数据,对于该应用来说,相当于全局变量

2、对数据进行分布式缓存

由于采用集中式存储,必然涉及网络传输或者持久化存储,所以在ASP.NET Core中,分布式缓存只支持字节数组这一种数据类型,应用程序需要自行解决针对缓存对象的序列化和反序列化问题。

分布式缓存的读和写实现在通过 IDistributedCache 接口表示的服务中。分布式缓存的操作除了设置、提取和移除,还包括刷新缓存。所谓的刷新是针对滑动时间过期策略而言的,每次刷新都会将对应缓存条目的最后访问时间设置为当前时间。

基于 Redis 数据库的分布式缓存具体实现在 RedisCache 类型上,它是对 IDistributedCache 接口的实现。

RedisCacheOptions 对象承载了与目标Redis数据库相关的配置选项。

设置分布式缓存条目时, DistributedCacheEntryOptions 对象用来决定设置的缓存条目何时过期。对象里同样的有绝对过期时间和滑动过期时间属性。

注意:Redis 是实现分布式缓存最好的选择

2.1、基于 Redis 的分布式缓存

首先请确保已经正常安装并启动了 Redis,并在项目中添加Nuget包:Microsoft.Extensions.Caching.Redis

        //startup.cs
        public void ConfigureServices(IServiceCollection services)
        
            services.AddDistributedRedisCache(option =>
            
                option.Configuration = "localhost";
                option.InstanceName = "Demo_";//每个rediskey前面加上的字符串,用于区分不同应用系统。
            );

            services.AddControllersWithViews();
        
        
        //HomeController.cs
        private readonly IDistributedCache distributedCache;//Redis
        public HomeController(IDistributedCache distributedCache)
        
            this.distributedCache = distributedCache;
        
        public async Task<string> TestRedisCache()
        
            var time = await distributedCache.GetStringAsync("CurrentTime");
            if (null==time)
            
                time = DateTime.Now.ToString();
                await distributedCache.SetAsync("CurrentTime", Encoding.UTF8.GetBytes(time));
                //设置带有过期时间的
                //redisCache.SetString("str", DateTime.Now.Ticks.ToString(),new DistributedCacheEntryOptions()  AbsoluteExpiration=DateTime.Now.AddSeconds(10));
            
            return $"缓存时间:time(,当前时间:DateTime.Now)";
        

option.InstanceName = "Demo_"; 这句,表示当多个应用共享同一个Redis数据库时,缓存数据可以利用该属性值进行区分不同应用的数据。当缓存数据被保存到Redis数据库中的时候,Key 自动在前面加上 InstanceName 的值,作为最终保存到Redis的Key。

可以额外了解下:StackExchange.Redis 。它是 .NET 领域知名的 Redis 客户端框架。

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

需要依赖 Nuget 包:Microsoft.Extensions.Caching.SqlServer

首先得有一个已经可用的 SQL server 数据库。存放数据的缓存表可以通过 cmd.exe 执行以下命令创建:

dotnet sql-cache create "server=.;database=MyTest;uid=sa;pwd=你的密码;" dbo AspnetCache

建立的表结构如下:

在应用程序中的编码:

		//startup.cs:
		public void ConfigureServices(IServiceCollection services)
                  

            services.AddDistributedSqlServerCache(options =>
            
                options.ConnectionString = "server=.;database=MyTest;uid=sa;pwd=你的密码;";
                options.SchemaName = "dbo";
                options.TableName = "AdpnetCache";
            );

            services.AddControllersWithViews();
        
        //HomeController.cs
        private readonly IDistributedCache distributedCache;//SqlServer
        public HomeController(IDistributedCache distributedCache)
        
            this.distributedCache = distributedCache;
        
        public async Task<string> TestRedisCache()
        
            var time = await distributedCache.GetStringAsync("CurrentTime");
            if (null==time)
            
                time = DateTime.Now.ToString();
                await distributedCache.SetAsync("CurrentTime", Encoding.UTF8.GetBytes(time));
            
            return $"缓存时间:time(,当前时间:DateTime.Now)";
        

这种情况下的缓存实现在 SqlServerCache 类型中,该类型由 Nuget 包:Microsoft.Extensions.Caching.SqlServer 提供。相关缓存配置信息由: SqlServerCacheOptions 对象提供。其属性:

  • ConnectionString :SQL数据库连接字符串
  • SchemaName 和 TableName :分别表示缓存数据表的Schema与表名称

与基于Redis数据库的分布式缓存相比,针对SQL Server数据库的分布式缓存与其差异最大的就是如何删除过期缓存条目。

3、缓存整个HTTP响应

上面的两种缓存,都要求利用注册的服务对象以手动方式存储和提取具体的缓存数据。响应缓存(Response Caching)则不再基于某个具体的缓存数据,而是将服务端生成的HTTP响应的内容予以缓存。响应缓存是http规范家族中的一个重要成员。

HTTP/1.1 Caching (缓存规范)

只针对方法为 GET 或 HEAD 的请求,这样的请求旨在获取URL所指向的资源或者描述资源的元数据。

缓存会根据一定的规则在本地存储一份原始服务器提供的响应副本,并赋予它一个保质期,保质期内的副本可以直接用来作为后续匹配请求的响应,所以缓存能够避免客户端与原始服务器之间不必要的网络交互。

私有缓存和共享缓存

私有缓存是客户端自己的缓存仅自己用,比如每个用户的浏览器上的缓存。

共享缓存是一份缓存可多人使用。这种类型的缓存一般部署在一个私有网络的代理服务器上,称为缓存代理服务器。

在响应报文中,使用 Cache-Control 报头区分私有缓存和共享缓存:Cache-Control: public|private

使用场景

  • 静态页面,几乎不怎么变的

如何在程序中使用

在 startup.cs 中:

  • ConfigureServices 方法里面加上:services.AddResponseCaching(); //需用到内存缓存

  • Configure 方法里加上:app.UseResponseCaching();

然后就可以在程序中(比如 HomeController)使用了:

 
		//HomeController.cs
        /// <summary>
        /// 把响应内容缓存到客户端,以便减少与服务器的网络交互。
        /// </summary>
        /// <returns></returns>
        public DateTimeOffset Response( [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)
            ;

            //让缓存的key不仅包含请求的路径(不含?之后的部分),还应该包括查询字符串“utc"和请求报头“X-UTC”的值。
            var feature = HttpContext.Features.Get<IResponseCachingFeature>()!;
            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;

测试方法:用浏览器打开多个相同URL的标签页,发现内容都一样。即除了首次访问服务器外,其它都是从本地磁盘缓存读取的响应。

屏蔽缓存

响应缓存通过复用已经生成的响应内容来提升性能,但不意味任何请求都适合以缓存的内容予以回复。请求携带的一些爆头会屏蔽响应缓存。更加准确的说法是客户端请求携带的一些报头。“提醒”服务端当前场景需要返回实时内容,例如携带authorization报头的请求,在默认情况下将不会使用缓存的内容予以回复。另外使用“Cache-Control:no-cache”或“Pragma:no-cache”的请求报头(冒号左边是key右边是value),则服务端会返回实时的响应内容。

ResponseCachingMiddleware与 http-cache

ASP.NET Core 的缓存响应是利用 ResponseCachingMiddleware 中间件实现的,该中间件按照 HTTP 规范来操作缓存。该规范只针对方法为GET或HEAD的请求。如果将资源的提供者和消费者称为目标服务器与客户端,那么所谓的缓存是位于这两者之间的一个HTTP处理部件。缓存会根据一定的规则在本地存储一份服务器提供的响应副本,并赋予他一个保质期。保质期内的副本可以直接用来作为后续请求的响应,所以缓存能够避免客户端与目标服务器之间不必要的网络交互。(其实请求还是来到了服务器的,只不过经过响应缓存中间件处理后就可能直接返回缓存内容了)

私有缓存和共享缓存。注意理解下,这里不过多摘录。响应报文以如下形式采用catche-control报头,区分私有缓存和共享缓存:Cche-Control:public|private

ResponseCachingMiddleware 中间件在默认情况下并不会将携带的查询字符串作为缓存键的组成部分。为了提供用于存储响应内容荷载的请求报头名称。目标服务器在生成最初响应时会将它们存储在一个名为vary的报头中。

到缓存决定存储该响应副本,学会提取响应的vary包头提供的所有请求报头名称,并将对应的值作为存储该响应副本对应key的组成部分。对于后续指向同一个url的请求,只有在他们具有一致的报头值的情况下,对应的响应副本才会被选择。

缓存的再验证,新鲜度检查,保质期,Max-Age/Expire

注意:响应缓存其实是把响应相关信息在server 内存中缓存的,请求还是到服务端了的,只不过对于可用的缓存,响应缓存中间件直接处理,取出响应内容返回给客户端了。

关于ResponseCachingMiddleware 这个中间件,最详细的解释,请参考《ASP.NET Core 6 框架揭秘》第22章。

最近更新于:2023-3-29


以上是关于ASP.NET Core中如何使用缓存的主要内容,如果未能解决你的问题,请参考以下文章

如何在 ASP.Net Core 使用 内存缓存

ASP.NET Core中如何使用缓存

ASP.NET Core 中的 Redis 缓存

如何在 ASP.NET core rc2 中禁用浏览器缓存?

在ASP.NET Core应用中使用IMemoryCache缓存

在ASP.NET Core应用中使用IMemoryCache缓存