缓存 Caching
大约 4 分钟
缓存 Caching
基本概念
- 缓存(Caching)是系统优化中简单又有效的工具,数据库中的索引等简单有效的优化功能本质上都是缓存。
- 缓存命中:cache hit,用已有的副本为某些到达缓存的请求提供服务
- 缓存命中率:由缓存提供服务的请求所占的比例被称为缓存命中率
- 缓存数据不一致:缓存中的数据跟数据库的数据出现了不一致, 即其中一方存在脏数据的现象
缓存控制
- RFC7324 是 HTTP 协议中对缓存进行控制的规范,其中重要的是 cache-control 这个响应报文头。
cache-control:max-age=60
表示服务器指示浏览器端“可以缓存这个响应内容60秒”- 服务端需要在 Controller 控制器的 Action 操作方法上添加
[ResponseCache(Duration = 60)]
特性
缓存中间件
- 如果安装了 “响应缓存中间件” ,那么
ASP.NET Core
不仅会继续根据[ResponseCache]
设置来生成cache-control
响应报文头来设置客户端缓存,而且会按照[ResponseCache]
的设置来对响应进行服务器端缓存。 - 优点:对于来自不同客户端的相同请求或者不支持客户端缓存的客户端,能降低服务器端的压力。
- 用法:添加
app.UseResponseCaching()
,在app.UseCors()
之后,app.MapControllers()
之前 - 服务器端响应缓存的限制:
- 无法解决恶意请求给服务器带来的压力
- 响应状态码为200的 GET 或者 HEAD 响应才可能被缓存
- 报文头中不能含有 Authorization、Set-Cookie 等
内存缓存
内存缓存的数据保存在当前运行的网站程序的内存中,是和进程相关的
多个不同网站是运行在 Web 服务器不同的进程中的,因此不同网站的内存缓存是不会互相干扰的
使用方法:
- 添加缓存服务
builder.Services.AddMemoryCache();
- 在
Controller
构造函数中注入IMemoryCache
接口,在Action
中调用IMemoryCache
的方法管理缓存
- 添加缓存服务
IMemoryCache
的方法:TryGetValue、Remove、Set、GetOrCreate、GetOrCreateAsync
[HttpPost] public ActionResult<Student> GetById(StudentRequestMessage msg) { Console.WriteLine("msg:" + msg); Student result = memory.GetOrCreate(msg.Id, entity => { Console.WriteLine("缓存中获取数据失败,创建新数据."); entity.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(5); return new Student("张三", 15); })!; return result; }
缓存的过期时间
- 控制缓存更新的策略:
- 在数据改变的时候调用Remove或者Set来删除或者修改缓存,优点:及时
- 设置过期时间:绝对过期时间、滑动过期时间
entity.AbsoluteExpirationRelativeToNow
entity.SlidingExpiration
- 同时设置绝对过期时间和滑动过期时间:
- 缓存会在更早的时间过期
- 可以设定滑动过期时间同时设置更晚绝对过期时间
缓存穿透
查询一个数据库中不存在的数据,由于缓存中也不存在该数据,查询请求就会到达数据库,造成数据库压力增大
string cacheKey = "Book" + id;//缓存键 Book? b = memCache.Get<Book?>(cacheKey); if(b==null)//如果缓存中没有数据 { //查询数据库,然后写入缓存 b = await dbCtx.Books.FindAsync(id); memCache.Set(cacheKey, b); }
解决方法:将查不到数据也作为查询结果保存在缓存中
// 如果数据库查询结果为 null, GetOrCreateAsync 依然会把 null 作为合法值保存到缓存中 var book = await memCache.GetOrCreateAsync(cacheKey, (e) => dbCtx.Books.FindAsync(id));
缓存击穿
- 如果缓存在短时间集中失效,有大量线程来重建缓存,造成后端负载过大
- 解决方案:
- 方案1:使用分布式互斥锁,只允许一个线程重建缓存,其他线程直接获取缓存
- 可能存在死锁、线程池堵塞风险、高并发性能大大下降
- 方案2:设置缓存永不过期,使用逻辑过期时间,使用异步方法修改缓存
- 内存占用大,代码复杂
- 方案1:使用分布式互斥锁,只允许一个线程重建缓存,其他线程直接获取缓存
缓存雪崩
- 如果缓存在集中的时间创建,则缓存项将集中过期引起大量的缓存击穿,大量请求将达到数据库
- 解决方法:
- 数据预热:在基础过期时间上增加一个随机的时间,使同时创建的缓存过期时间相互错开
- 多级缓存
- 限流
缓存数据混乱
- 原因:不同的数据使用了相同的 key
分布式缓存
将多个 Web 应用服务器的内存缓存移到统一的缓存服务器上
避免不同的 Web 应用服务器向数据库发送相同的查询请求,降低数据库压力
.NET Core 中提供了统一的分布式缓存服务器的操作接口 IDistributedCache
分布式缓存和内存缓存的区别:缓存值的类型为 byte[]
Redis:适合在数据量大、高可用性等场合使用
// Microsoft.Extensions.Caching.StackExchangeRedis builder.Services.AddStackExchangeRedisCache(options => { options.Configuration = "localhost"; options.InstanceName = “ws_”; // 增加前缀避免数据混乱 });