缓存

你是我想和全世界炫耀,又舍不得和任何人分享的人。 --zhu
缓存定义
缓存(Caching)是系统优化中简单又有效的工具,投入小收效大。数据库中的索引等简单有效的优化功能本质上都是缓存。

多级缓存
浏览器-->网关服务器-->Web服务器-->数据库服务器。

客户端响应缓存
1、RFC73224是HTTP协议中对缓存进行控制的规范,其中重要的是cache-control这个响应报文头。服务器如果返回cache-control:max-age=60,表示服务器指示浏览器端“可以缓存这个响应内容60秒”。
2、我们只要给需要进行缓存控制的控制器的操作方法添加ResponseCacheAttribute这个Attribute,ASP.NET Core会自动添加cache-control报文头。

public class TestController:ControllerBase
{
  [ResposeCache(Duration =20)]
  [HttpGet]
  public DateTime Now()
  {
    return DateTime.Now;
  }
}

服务器端响应缓存
1、如果ASP.NET Core中安装了“响应式缓存中间件”,那么AP.NET Core不仅会继续根据[ResponseCache]设置来生成cache-control响应报文头来设置客户端缓存,而且服务器端也会按照[ResponseCache]的设置来响应进行服务器端缓存。和客户端缓存区别:来自多个不同客户端的相同请求。
2、“响应缓存中间件”的好处:对于来自不同客户端的相同请求或者不支持客户端缓存的客户端,能降低服务器端的压力。
3、用法:app.MapControllers()之前加上app.UseResponseCaching()。确保app.UseCors()写到app.UserResponseCaching()之前。
4、问题:
1)无法解决恶意请求给服务器带来的压力。
2)限制较多:响应码为200的GET或者HEAD响应才可能被缓存;报文头中不能含有Authorization、Set-Cookie等。
3)建议采用内存缓存、分布式缓存。

内存缓存(In-memory cache)
1、把缓存数据放到应用程序的内存。内存缓存中保存的是一系列的键值对,就像Dictionary类型一样。
2、内存缓存的数据保存在当前允许的网站程序中,是和进程相关。因为在Web服务器中,多个不同网站运行在不同的进程中,不同网站的内存缓存不会相互干扰,网站重启后,内存缓存中所有数据被清空。

内存缓存用法
1、启用:builder.Services.AddMemoryCache()
2、注入IMemoryCache接口,查看接口的方法:TryGetValue、Remove、Set、GetOrCreate、GetOrCreateAsync
3、用GetOrCreateAsync讲解

public async Task<Book[]> GetBooks()
{
  logger.LogInformation("开始执行GetBooks");
  var items = await memCache.GetOrCreateAsync("AllBooks",async(e)=>
  {
    logger.LogInformation("无缓存数据,从数据库获取");
    return awwit dbCtx.Books.ToArrayAsync();
  });
  logger.LogInformation("把数据返回给调用者");
  return items;
}

缓存的过期时间
1、以上缓存不会过期,只能重启服务器。
2、解决方法:在数据改变的时候调用Remove或者Set来删除或者修改缓存(优点:及时);过期时间(只要过期时间比较短,缓存数据不一致的情况也不会持续很长时间)。
3、绝对过期时间、滑动过期时间。

绝对过期时间
1、GetOrCreateAsync()方法的回调方法中有一个ICacheEntry类型的参数,通过ICacheEntry对当前的缓存项做设置。
2、AbsoluteExpirationRelativeToNow用来设定缓存项的绝对过期时间。

public async Task<Book[]> GetBooks()
{
  logger.LogInformation("开始执行GetBooks");
  var items = await memCache.GetOrCreateAsync("AllBooks",async(e)=>
  {
    logger.LogInformation("无缓存数据,从数据库获取");
    e.AbsoluteExpirationRelativeToNow =TimeSpan.FromSeconds(10);//缓存有效期10秒
    return awwit dbCtx.Books.ToArrayAsync();
  });
  logger.LogInformation("把数据返回给调用者");
  return items;
}

滑动过期时间
1、缓存没过期时再请求一次,缓存续命一次。
2、ICacheEnty的SlidingExpiration属性用来设定缓存项的滑动过期时间。

public async Task<Book[]> GetBooks()
{
  logger.LogInformation("开始执行GetBooks");
  var items = await memCache.GetOrCreateAsync("AllBooks",async(e)=>
  {
    logger.LogInformation("无缓存数据,从数据库获取");
    e.SlidingExpiration = TimeSpan.FromSeconds(10);//缓存有效期10秒
    return awwit dbCtx.Books.ToArrayAsync();
  });
  logger.LogInformation("把数据返回给调用者");
  return items;
}

两种混用
使用滑动过期时间策略,如果一个缓存项一直被频繁访问,那么这个缓存项就会一直被续期不过期。可以对一个缓存项同时设定滑动过期时间和绝对过期时间,并且把绝对过期时间设定的比滑动过期时间长,这样缓存项的内容会在绝对过期时间内随着访问被滑动续期,但一旦超过绝对过期时间,缓存项就会被删除。

public async Task<Book[]> GetBooks()
{
  logger.LogInformation("开始执行GetBooks");
  var items = await memCache.GetOrCreateAsync("AllBooks",async(e)=>
  {
    logger.LogInformation("无缓存数据,从数据库获取");
    e.SlidingExpiration = TimeSpan.FromSeconds(30);
    e.SlidingExpiration = TimeSpan.FromSeconds(10);
    return awwit dbCtx.Books.ToArrayAsync();
  });
  logger.LogInformation("把数据返回给调用者");
  return items;
}

内存缓存总结
1、无论用哪种过期时间策略,程序中都会存在缓存数据不一致的情况。部分系统(博客)无所谓,部分系统不行(金融)。
2、可以通过其他机制获取数据源改变消息,哉通过代码调用IMemoryCache的Set方法更新缓存。

缓存穿透

string cacheKey="Book"+id;
Book? b=memCache.Get<Book?>(cacheKey);
if(b==null)如果缓存中没有数据
{
  b = await dbCtx.Books.FindAsync(id);//查询数据库,写入缓存
  memCache.Set(cacheKey,b);
}

上述代码,遇到不存在缓存键值时,会查询数据库,如果有人恶意频繁传入不存在的Id,每次都要查数据库,给数据库造成压力。

缓存穿透解决方法
1、把数据库查不到的当成一个指定数据放入缓存。
2、我们用GetOrCreateAsync方法即可,因为它会把null值当成合法的缓存值。

string cacheKey = "Book"  + id;
var book =await memoCache.GetOrCreateAsync(cacheKey,async(e) =>{
  var b=await dbAtx.Books.FindAsync(id);
  logger.LogInformation("数据库查询:{0}",b==null?"为空":"不为空");
  return b;
 });
logger.LogInformation("Demo5执行结束:{0}",b==null?"为空":"不为空");

缓存雪崩
1、缓存项集中过期引起缓存雪崩。(例如所有都堆30秒后过期)
2、解决方法:在基础过期时间上,再加一个随机过期时间。

e.AbsoluteExpirationRelativeToNow =TimeSpan.FromSeconds(Random.Shared.Next(10,19));//过期时间随机,(Random.Shared全局随机数)

缓存数据混乱

public User GetUserInfo()
{
  Guid userId=...;
  return memCache.GetOrCreate("UserInfo",(e)=>
  {
    return ctx.User.Find()userId;//要不重复的键,UserInfo改成UserInfo+Id
  })
}

分布式缓存
内存缓存效率高,但是如果集群节点数量非常多的话,重复查询时还是会把数据库整垮。
1、常用的分布式缓存服务器有Redis、Memcached等。
2、.NET Core中提供了统一的分布式缓存服务器操作接口IDistributedCache,用法和内存缓存类似。
3、分布式缓存和内存缓存的区别:缓存值的类型为byte[],需要我们进行类型转换,也提供了按照string类型存取缓存值的扩展方法。

缓存服务器选择
1、SQLServer做缓存性能不好。
2、Memcached是缓存专用,性能非常高,但是集群、高可用等方面比较弱,而且“缓存键的最大长度为250字节”限制。安装EnyimMemcachedCore包。
3、Redis不局限于缓存,Redis做缓存服务器比Memcached性能稍差,但是Redis的高可用、集群等方面强大,适合数据量大、高可用性等场景。

使用
1、NuGet安装Mircrosoft.Extensions.Caching.StackExchangeRedis
2、

builder.Services.AddStackExchangeRedisCache(options=>
{
    options.Configuration="localhost";
    options.InstanceName="yzk_";//避免混乱
});
public async Task<Book[]> GetBooks(long id)
{
  string? s = await distCache.GetStringAsync("Book"+id);
  if(s==null)
  {
    var book=await MyDbContext.GetByIdAsync(id);
    await distCache.SetStringAsync("Book"+id,JsonSerializer.Serialize(book));
  }
  else
  {
    book = JsonSerializer.Deserialize<Book?>(s);
  }
  if(book==null)
  {
    return NotFound("不存在");
  }
  else
  {
    return book;
  }
}
posted @ 2024-07-27 20:59  小脑虎爱学习  阅读(115)  评论(0)    收藏  举报