一、知识来源
Part4-23:程序员的性能优化万金油:缓存
Part4-24:ASP.NET Core客户端响应缓存
Part4-25:鸡肋的ASP.NET Core服务器端响应缓存
Part4-26:ASP.NETCore中的内存缓存
Part4-27:ASP.NETCore缓存的过期时间策略
Part4-28:ASP.NET Core缓存穿透的问题
Part4-29:ASP.NETCore缓存雪崩的问题
Part4-31:ASP.NETCore分布式缓存
Part4-32:封装分布式缓存操作的帮助类
二、抛砖引玉:数据的性能优化
1、数据库性能优化的万金油:索引
- 索引是一个投入小,收效大的优化方式。
- 索引优化主要是针对查询场景,把需要经常查询的字段加上索引,那么查询的效率就会大大提升。
三、Asp.Net Core开发优化万金油:缓存
1、在Asp.Net Core开发中的缓存也是一种性价比极高的优化方式。
2、什么是缓存
- 索引本质上就是缓存。
- 也有一些其它的简单有效的优化功能本质上也是缓存,这里不做展开。
- 数据库的内部也存在缓存的机制,这里只对程序层面的缓存展开讲解。
- 所谓的缓存顾名思义,就是把一些数据按照某种存储机制在内存中(或硬盘)进行临时存放。
3、缓存的机制(详见2中图)
每当在程序层面向数据库发起查询请求,在真正与数据库产生交互前会在缓存中查找是否存在需要查找的数据,如果有直接返回结果不用继续与数据库产生交互,如果没有就与数据库产生交互,并把返回数据放回缓存中。
4、为什么缓存的效率那么高?
如上所说,缓存有可能是存放在内存中(或硬盘),而数据库的信息是在另外一台服务器的的硬盘中。显而易见,访问本地内存的效率远远大于跨网络访问另外一台服务器。
5、缓存的优点
- 如4中解析,效率极高、访问速度极高。
- 有一部分请求是没有走到数据库的,只在本地与缓存交互。可以大大降低高频率访问数据库给数据库带来的压力。
6、相关概念及解析
缓存命中:用户发起一次查询请求,如果在缓存中查到了用户需要的数据那就是缓存命中,反之就是未命中。
缓存命中率:在经历N次查询请求后,命中的缓存的占所有查询请求的比例是多少。命中率与系统性能呈正比例。。所以提高缓缓命中率是程序员优化系统的一个重要方向。
缓存数据不一致:对于同样的查询条件,在缓存中查询的数据与在数据库中查询的数据不一致。产生的原因举例说明:缓存数据是在17:00被存入缓存,18:00数据库的数据库被执行了Update操作,但是因为缓存中有数据,所以从18:00之后所有的请求一直到缓存释放前查询的结果都会与实际的有出入。
-
多级缓存:在整个数据流转的链路中,每个结点可能都会有属于自己的缓存,如下图所示。结合缓存的机制(三-3)所说,因为每个结点都存在缓存,所以每次请求走的链路长度可能都不一致,有可能在网关服务器缓存查到数据就直接返回到浏览器了,不会执行流转到web服务器和数据库服务器,或者在web服务器直接返回到浏览器。
四、客户端响应缓存
1、在Asp .Net Core项目中,设置客户端缓存并设置缓存最大保存时间。
- 如果没有设置[ResponseCache(Duration = 20)],那么每次获取的时间就当前最新时间
- 如果设置了,那么在20秒内都不会获取当前最新时间,而是直接从缓存中获取之前保存的”当前最新时间“
- 20秒后缓存销毁,然后获取新的当前时间存入缓存。
[ResponseCache(Duration = 20)]
2、通过F12查看发起的请求验证在20秒内:除了第一次请求时打到了服务器内,其它的只是与缓存交互。
-
disk cache:意思是从浏览器缓存中获取到数据。
-
过20秒继续访问请求:是从服务器中重新获取的数据
3、查看浏览器报文头:cache-control
请求与缓存的交互:状态码会有备注
-
请求头遵守cache-control: public,max-age=20:如本章最开始的图片所描述,这里是给浏览器建议可以缓存20秒,浏览器也可以不遵守。
-
在浏览器内手动设置停用缓存,不遵守让浏览器缓存20秒的规则:禁用缓存。每次请求都是与服务器交互。
-
请求与浏览器的交互:
4、也可以通过在代码中打断点验证在20秒内:除了第一次请求时代码走到了断点处,其它的压根儿没有走到断点处,说明只是与本地缓存交互。
五、服务器端缓存
1、服务器缓存在请求的链路中,介于客户端缓存和服务器之间
2、服务器缓存和客户端缓存区别
- 不同客户端的客户端缓存内容不同,每个客户端使用的服务器缓存都是相同的。
- 例如:假设ABC三个客户端分别对同一个接口发起3次请求,如果ABC各自有客户端缓存没有服务器缓存,那么此时ABC三个客户端分别对服务器接口发起了1次请求,各自剩下的两次请求命中了客户端缓存;如果ABC各自有客户端缓存,且“同一个接口”设置了服务器缓存,那么ABC三个客户总的9次请求中,只有1次能打到服务器内,其余的要么是命中了客户端缓存,要么命中了服务器缓存。
-
总体而言:设置了服务器缓存后,实际能打到服务器的请求的数量是少于只设置了客户端缓存
3、在Asp .Net Core项目中,设置服务器缓存并设置缓存最大保存时间。
-
如果涉及到跨域的配置,设置服务器缓存的代码一定要放在配置跨域的代码后面。
4、配置服务端缓存后,验证演示
-
开启两个浏览器,分别访问GetNowTime接口
返回时间一致,说明二者都命中了服务器端缓存。
5、关闭服务端缓存后,验证演示
-
同样是开启两个浏览器,分别访问GetNowTime接口
返回时间不一致。
6、验证演示注意事项
-
因为RFC7324规范原因,如果要验证服务器端缓存,需要开启两个浏览器,如4、5中所示,用两个客户端是要消除客户端缓存的干扰,同时保留服务器端缓存。
-
通过设置浏览器端的禁用缓存能达到屏蔽浏览器缓存的目的吗?不能,因为一旦在浏览器设在禁用缓存,那么请求的请求头就会出现“cache-control: no-cache”,这个会导致浏览器缓存和服务器端缓存都失效,所以每次请求都会打到Action方法中,就无法验证请求是否打到了服务器缓存中。
-
解决RFC7324规范带来的验证干扰,也可用PostMan解决,但是记得设置如下图框出的地方,设置完后请求头就不包含“cache-control: no-cache”,这个地方的设置可以在禁用客户端缓存的同时不禁用服务器端缓存,同时因为设置了服务器缓存,所以在一段时间内,走postMan对GetNowTime请求,返回的都是同一个时间。就能够达到本次演示的目的。
总结:只要请求头包含
cache-control: no-cache
,就会忽略服务器端缓存,请求直接打到Action中。是否在请求头中添加cache-control: no-cache
,在postman中可以设置Send no-cache headers
、在浏览器端可以设置禁用缓存。
7、服务器端缓存很鸡肋
无法减低服务器压力:因为不论是从PostMan还是浏览器发起请求,都可以非常简单轻松的通过在请求头中配置
cache-control: no-cache
来使得服务器端缓存失效,一旦有恶意客户端这样做并多次发起与数据库交互的请求,就会因为服务器端缓存失效而使得缓存的优势失效,导致数据库压力剧增。服务器端缓存还有很多限制:如图
本质上鸡肋的原因是因为严格遵守了RFC7324协议
8、用不用服务器端缓存?因为比较鸡肋,极少数情况下可以使用,比如B端的系统,不用担心恶意攻击。
9、那么用什么缓存?更推荐使用内存缓存!
六、内存缓存
1、运行机制
- 内存缓存中的内容都是键值对,根据键,可以从内存中取出值。
- 在同一个进程里面,内存里面的数据是有相同生命周期的。
- 程序一旦关闭,与程序同一个进程里面的缓存占用的内存、用户代码占用的内存都会被释放,即内存、缓存数据会被销毁。
- 同一个服务器中,不同的web项目运行在不同线程中,所以不同的web项目之间的内存缓存不会相互干扰导致混乱。
- 重启服务器会内存缓存数据被销毁。
- 当内存中有缓存时,路线为:1→2→3-1→4
- 当内存中没有缓存时,路线为:1→2→3-2→3-3→3-4→4
2、内存缓存优势
- 解决了服务器缓存和客户端缓存能够被用户轻易屏蔽的弊端
- 与内存交互,效率极高。
2、使用方法
3、功能部分的代码
-
抽象类Book
-
假设是一个从数据查询的类:MyDbContext,里面有一个伪异步方法和同步方法,二者功能相同
-
获取书本的查询接口
4、如何没有设置内存缓存,每次调用GetBookById接口,都会打到服务器中接口的代码,即使请求的都是同一个id。
5、通过IMemoryCache增加内存缓存
-
IMemoryCache的方法:CreateEntry创建缓存内容、Remove删除缓存、TryGetValue根据Key从缓存中去取出需要的值。
-
IMemoryCache的扩展方法
-
使用IMemoryCache前的配置:在Program.cs中注入AddMemoryCache()这个内存缓存服务、在接口中通过依赖注入创建IMemoryCache类型对象
6、使用IMemoryCache的扩展方法GetOrCreateAsync来讲解:新增一个与GetBookById(获取书本的查询接口 )相同功能的Action:GetBookByIdUseIMemoryCache,区别是里面使用了内存缓存
- GetOrCreateAsync功能详解:可用于检查缓存是否包含指定键的项,如果存在,则返回该项;否则会创建一个新项并将其添加到缓存中。
- 下面是一个使用MemoryCache实现的简单的GetOrCreateAsync方法的源代码示例,可以在C#缓存中使用。该方法接受一个缓存键和一个委托,用于创建并返回要存储在缓存中的对象。这里代码可以结合委托笔记来学习理解。
csharp
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
public static async Task<TItem> GetOrCreateAsync<TItem>(this IMemoryCache cache, object key, Func<Task<TItem>> factory)
{
if (!cache.TryGetValue(key, out TItem result))
{
result = await factory();
cache.Set(key, result);
}
return result;
}
-
GetOrCreateAsync的功能等同于get+set,但是get+set(即以下代码)会带来缓存穿透问题,GetOrCreateAsync可以完美解决缓存穿透问题,所以还是建议只使用GetOrCreateAsync,第八章会展开讲解。
GetOrCreateAsync参数详解:
key (必需): 表示要获取或创建并添加到内存缓存中的项的缓存键
factory (必需): 一个异步回调函数,该函数将负责创建要添加到缓存中的新项,该委托将返回相应类型的值。
expirationTime (可选): 一个TimeSpan值,在此时间段后,将自动从缓存中删除该项。如果未设置过期时间,则该项将保持缓存状态,直到手动删除它为止。
如果从缓存中找不到对象,你可以选择把factory 另一個參數加入 isSlidingExpiration(boolean) ,表明这个记录是不是要判断Sliding Expiration模式-
实现接口
7、执行
-
第一次执行GetBookByIdUseIMemoryCache接口,并查看日志:进入到了回调函数
-
第一次执行GetBookByIdUseIMemoryCache接口,并查看日志:没有进入回调函数,直接从缓存中取的数据
8、关于GetOrCreateAsync的Key参数需要注意的地方
因为从内存缓存中取数据是通过键值对的形式,所以Key一定要取唯一值,一旦取得不是唯一值,就会出现获取的数据不符合预期
-
还是以GetBookByIdUseIMemoryCache方法为例,将"Book" + id参数改成“Book”,然后分别以id=1和id=2
调用GetBookByIdUseIMemoryCache接口。可以发现id=2的返回数据竟然是id=1的数据,这是因为第一次调用id=1后,在缓存内存中保存的键值对是:{"book":{"d="1",name="java"}}
,那么调用id=2时,因为GetBookByIdUseIMemoryCache的key参数是写死的“book”,不是动态的id,所以返回的是id=1的数据。
具体key如何设定,与实际业务场景相关,比如采购业务中的PO单号就是唯一的,PO单号可以作为Key。
七、解决缓存与数据库数据不一致问题
1、说明:假设查询id=1的书籍信息,然后被存入内存缓存,接着在数据库中更新id=1的书籍信息,然后再查询id=1的数据,因为内存缓存的存在,获取的是内存缓存中的数据,而非数据库中的数据,但是id=1的书籍此时内存缓存内的数据与数据库的数据不一致了。这种情况肯定不是想看到的。
2、解决方案:让内存缓存数据在合适的时机更新为数据库内的数据(新数据),保证缓存与数据库一致性:目前可以提供两种具体的解决方案。
- 方法一:及时修改缓存内容,优点是很及时,但是写代码很麻烦
-
方法二:设置过期时间,假设设置过期时间5秒,那么5秒后就更新缓存内存。设置很简单,但是依然会偶然存在缓存不一致问题。默认过期时间是永远,除非服务器重启。
3、设置过期时间:绝对过期时间,AbsoluteExpirationRelativeToNow
-
ICacheEntry类型参数
-
ICacheEntry的属性介绍:主要关注AbsoluteExpirationRelativeToNow
-
设置绝对过期时间的代码
4、设置过期时间:滑动过期时间,SlidingExpiration
-
释义:滑动过期时间是指在访问缓存数据时,如果该数据被访问,则将缓存时间重置,并延长指定的时间。假设设置的滑动过期时间是10秒,第一查询后查询结果加入到缓存,在距离第一次查询后的第9秒再查询,那么缓存将会以第二次查询时间为基准再次设置过期时间为10秒,接着在距离第二次查询后的第11秒进行第三次查询,那么此时距离第二次查询过去了10秒,那么缓存内容就会被销毁,就会重新走到服务器并将新查询结果存入缓存。
-
代码
5、过期时间混用:绝对过期时间+滑动过期时间
-
释义:如果访问的频率足够的频繁,那么滑动过期时间的策略下,缓存会永远不过期,这并不是一种好的情况,此时用绝对过期时间策略保证在到达某个时间点后缓存一定会过期然后被更新。
代码
e.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(30);
e.SlidingExpiration = TimeSpan.FromSeconds(10);
- 注意:绝对过期时间和滑动过期时间都会导致缓存过期,以代码中的设置为例,如果第二次查询距离第一次查询超过了10秒,那么此时会因为滑动过期时间设置为10秒导致缓存过期。如果每次查询时间都是10秒内,因为滑动过期时间设置为10秒,所以滑动过期时间的存在不会导致缓存过期,但是因为有绝对过期时间30秒的存在,所以一旦最后一次查询与第一次查询超过30秒,那么此时就会导致缓存过期。
6、内存缓存总结
- 大多数情况下用绝对过期时间策略,设置一个相对较短的过期时间
- 偶尔使用混用过期时间策略
-
单独使用滑动过期时间策略几乎没有
八、缓存穿透
1、以下代码其实和GetOrCreateAsync的功能一致,只不过是将其拆分成了get和set。
-
功能解释:从缓存中查询数据(get),如果查询结果为空就去数据库查并将查询结果写入缓存(set)。
2、在上述代码中存在一个致命的缺陷,即当查询id=3的书籍信息时,如果缓存中没有缓存结果,将会调用数据库进行查询。如果在数据库中也没有该书籍信息,则将会得到一个空结果并将其写入缓存,在下一次查询id=3的请求到来时依然不会从缓存中获取数据而是继续进入数据库。这种情况被称为“缓存穿透”。
3、缓存穿透的危害:恶意用户会利用缓存穿透特性,制造一些不存在于数据库中的书籍id,那么每次查询都会去到数据库中,再高频率调用查询书籍信息的接口,来给数据库造成极大的压力。
4、利用缓存穿透使得数据库压力剧增的攻击方式,类似于在请求头中添加“cache-control: no-cache”的行为,绕过客户端和服务器的缓存以便每次查询都直接访问数据库。要避免这种危害, 内存缓存(缓存)是常用的解决方案之一。但即使使用内存缓存仍然可能面临恶意用户利用缓存穿透的攻击方法来绕过内存缓存并向数据库发起攻击。
5、缓存穿透解决方案
- 引起缓存穿透的核心原因就是null既代表了缓存中没有值,也代表了数据库中没有值。
- 那么可以使用别的标识来代表数据库中没有值,使得null仅代表缓存中没有值。
-
GetOrCreateAsync虽然功能等于get+set,但是GetOrCreateAsync自动的会去区分从数据库中查询的null不等同于从缓存中查找值为null,也就是说虽然从数据库中查询的值是null,并且把这个所谓的null返回值保存到了缓存,但是下次从缓存中找到这个”null“不会判定为缓存中没有值,而是判定为缓存中有这个值,只是从数据库中查询的结果是null。
6、验证GetOrCreateAsync解决缓存穿透
-
可以发现第一次查询id=6的信息是没有从数据库查询到,返回的是null,但是第二次查询就不会走到数据库查询中。
7、总结:GetOrCreateAsync就可以直接解决get+set带来的缓存穿透问题,直接正常使用即可,不需要新增任何其他的代码,底层就已经帮我们做好了这些事情。所以我们在用内存缓存的时候,只用GetOrCreateAsync方法,而不要使用get+set。
九、缓存雪崩
1、定义:缓存雪崩(Cache Avalanche)是指当缓存中的大量数据同时失效或某个关键数据过期/异常时,会导致全部请求直接访问底层数据库或后端系统,从而导致数据库过载、应用服务器响应时间延迟等一系列问题。
2、因为设置了一致的过期时间导致的缓存雪崩场景说明:在电商双十一零点抢购场景中,用户集中访问了1000个商品,此时因为内存缓存存在,先是在数据库中查询了1000次,同时1000个商品都被存到了缓存中,同时这1000个商品在缓存中的过期时间统一被设置成了5秒,也就是说再接下来的5秒内对这1000个商品的访问请求都会命中缓存不会对数据库造成压力,但是5秒后,这个1000个商品在缓存内全部被清除,清除后用户再次同时访问这1000个商品时,那么就会同时进入到数据库中查询,此时对数据库的压力就会巨增加。依次往复,每隔5秒就会对数据库带来巨大的查询流量,数据库的压力是呈现波动状态,1-4秒压力为0,5秒压力1000,6-9秒压力为0,10秒压力1000。
-
当然,还有
3、以上场景带来的问题:在遇到峰值的时候可能会压垮数据库。
4、2中的图波动太大,如果能将峰值压力分摊到每一秒就非常好,下图是黄线是符合预期的压力随时间变化的趋势。
5、除了设置一致的缓存过期时间可能导致的缓存雪崩外,还有以下几种原因可能造成缓存雪崩:
- 硬件故障或宕机:如磁盘故障、网络故障等,因为缓存通常是存在于内存中的,如果出现硬件故障或宕机问题,可能会导致数据丢失并触发缓存雪崩。
- 热点数据:当某些数据访问频率过高时,就会形成热点数据。如果这些热点数据都在同一时刻过期或失效,则大量请求都会落到数据库上,导致数据库压力骤增,进而引起数据库崩溃,从而触发缓存雪崩。
- 缓存服务端内存泄漏:如果缓存服务端存在内存泄漏问题,就会导致系统内存资源逐渐耗尽,最终挂掉,形成缓存雪崩。
- 分布式锁失效:为了避免缓存雪崩情况,很多人采用分布式锁来保证只能有一个线程去加载数据或重新生成缓存,但如果分布式锁失效或出现其他异常情况也会导致缓存雪崩。
- 但是本章节学习仅以设置一致的缓存过期时间可能导致的缓存雪崩为例。
6、设置一致的缓存过期时间导致的缓存雪崩解决方案
- 如1中的图片第二条所示,不要设置一致的过期时间,而是在一个范围内的随机值。
- 还是以2中例子为准,设置过期时间就不要统一设置5秒,而是设置成5到10秒之间,假设有1000条数据,那么从统计学理论上看,缓存时间为5、6、7、8、9、10的数据条数应该是接近一致的。
-
代码演示:e.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(Random.Shared.Next(5,10));
十、封装内存缓存操作的帮助类
1、解决的问题
- 检查数据合法性,禁止使用IQueryable、IEnumerable类型
-
实现随机缓存过期时间,即使传入的是固定的缓存过期时间。
2、代码
源码地址NETBookMaterials/MemoryCacheHelper.cs at main · yangzhongke/NETBookMaterials (github.com)
安装的包:Zack.ASPNETCore
注册服务
builder.Services.AddScoped<IMemoryCacheHelper, MemoryCacheHelper>(); //使用杨中科老师重写在Zack.ASPNETCore中的MemoryCacheHelper
-
接口:使用起来和官方的没有任何区别
十一、分布式缓存
1、内存缓存的优缺分析
- 优点:使用简单、部署简单、效率高效。在服务器数量不多、数据库压力不大的情况下,内存缓存优点大于缺点。
- 缺点:没法跨集群结点复用缓存内容。以下内容具体举例说明:假设系统访问量很大,系统内部署非常多的集群,比如几百上千个集群实例,每个集群的内存缓存都在各自的集群上,此时同一个接口的请求被发起了很多次,然后分别去到不同集群,因为各自集群使用的内存缓存并不共享,所以有时候即使某个请求返回的数据在集群A中的内存缓存中存在,同样的请求去到了集群B,但是集群B的内存缓存没有这个请求的缓存数据,此时这个查询依然会去到数据库,一但在访问量大的情况下,数据库依然存在被轻松击垮的可能性。
2、分布式缓存解决内存缓存缺点
- 使用集群缓存的场景是:集群结点数量非常多、系统访问量存在非常大的时刻。
- 一般情况下,能够使用内存缓存还是优先使用内存缓存,非必要情况下选择分布式缓存。
3、分布式缓存架构图
- 缓存服务器这里仅显示一台,实际场景中可能存在多台的情况构成分布式情况,但是逻辑上多台服务器缓存还是一台。
- 所有web服务器不再将各自缓存放置各自的内存中,而是读取、写入缓存数据都是在缓存服务器中操作,缓存服务器对于所有的web服务器是一个共享中心,A服务器存入到缓存服务器中的缓存数据,BC服务器也能够读取到。那么此时缓存服务器被称为:集中分布式缓存服务器。
4、分布式缓存服务器
- 在aps .net中微软并没有开发一个分布式缓存服务器,因为市面上已经有了非常多的成熟的、主流的分布式缓存服务器,微软只是定义了一个接口,帮助大家更简单的操作分布式缓存服务器。
- 目前主流的分布式缓存服务器有:Redis、Memcached。
- 也可以使用数据库做分布式缓存服务器,但是性能弱,几乎不使用。
- 因为数据库、Redis、Memcached这些分布式缓存服务器用法均不同,所以.net提供了一个统一的公共接口IDistributedCache,在Aps.net中无论使用那个分布式缓存服务器都通过这个公共接口进行。
- 注意:分布式缓存服务器中,IDistributedCache接口统一要求缓存值得类型是byte[],因为分布式缓存是跨服器的通讯,所以必须得这么限制缓存值类型。
5、用什么做缓存服务器
-
根据下图总结,最推荐使用Redis
6、分布式缓存用法之Redis
- 安装包:NuGet\Install-Package Microsoft.Extensions.Caching.StackExchangeRedis -Version 7.0.2
- 注册服务
//使用Redis分布式缓存
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "localhost"; //连接哪个redis服务器
options.InstanceName = "rly_"; //避免分布式缓存的key与其他项目的key混乱,建议大家给key添加一个前缀
});
-
功能函数中使用分布式缓存:如4中所说,分布式缓存中缓存值的类型都是byte[],所以在保存缓存的时候需要将缓存值先序列化、从缓存中查到的缓存值要反序列化
7、验证效果
-
对id=2的书籍查询两次
查看redis中id=2的书籍的缓存值
其中key组成为:前缀(rly)+Book+id,
Book和id是在保存缓存值的函数SetStringAsync中设置的
await disCache.SetStringAsync("Book" + id,JsonSerializer.Serialize(book));
.可往上查看代码前缀rly是在Programs的注册服务中用
options.InstanceName = "rly_";
设置的。可往上查看代码
8、分布式缓存+redis也可以默认解决缓存穿透问题
-
先查询一个不存在的数据id,并查询两次,对于不存在的数据每次查询都是走缓存,而不是缓存穿透走到数据库中。
-
对于不存在的数据,在Redis中保存的数据:将null值也作为一种值保存到了缓存,这里的nul是字符串类型的null。
总结:使用分布式缓存+redis可以不用担心缓存穿透的问题,正常使用就行。
十二、分布式缓存帮助类(杨中科老师提供的包)
1、解决的问题
2、源码地址
NETBookMaterials/DistributedCacheHelper.cs at main · yangzhongke/NETBookMaterials · GitHub
3、使用方式
安装的包:Zack.ASPNETCore
注册服务
//分布式缓存:使用杨中科老师写的帮助类
builder.Services.AddScoped<IDistributedCacheHelper, DistributedCacheHelper>();
-
代码
十三:总结
内存缓存性能优于分布式缓存,除非必要,否则还是优先使用内存缓存。