.NET006-缓存


BackGround

服务器在极短时间内重复遇到请求,导致服务器奔溃,如何优化?

Theory

Google直接搜索Caching,出来第一项解释:Caching is a technique that stores a copy of a given resource and serves it back when requested. When a web cache has a requested resource in its store, it intercepts the request and returns a copy of the stored resource instead of redownloading the resource from the originating server.什么意思呢?缓存是一项技术,用于存储给定资源的拷贝并在下一次请求的时候将拷贝内容提供回去。当一个Web缓存在存储中有请求的资源,它将会截获该请求并返回存储值而不是从原始服务器重新下载。

就优化系统而言,缓存是简单又有效的工具,投入成本小但收获效益高,数据库的索引本质上也是缓存。缓存有三个重要概念:缓存命中、缓存命中率、缓存数据一致性。在高并发的系统中,缓存的命中率至关重要。在实际开发过程中考研用zabbix、MemAdmin等工具直观的监控。从架构师的角度,需要应用尽可能的通过缓存直接获取数据,并避免缓存失效。这也是比较考验架构师能力的,需要在业务需求,缓存粒度,缓存策略,技术选型等各个方面去通盘考虑并做权衡。尽可能的聚焦在高频访问且时效性要求不高的热点业务上,通过缓存预加载(预热)、增加存储容量、调整缓存粒度、更新缓存等手段来提高命中率。

四种缓存方式

  1. 客户端缓存
    打开浏览器客户端,按F12(一般而言)进入开发者模式,勾选Disable cache则禁用客户端缓存。后端API上只需要添加ResponseCacheAttribute即可,ASP.NET Core会自动提价cache-control的报文头。
  2. 服务器端缓存
    可以理解包在服务器外围,主要用于多客户端来访问同一个服务器的情况,能够降低服务器压力。当然比较鸡肋,按照RFC7324规范,只要客户端禁用了缓存服务器端缓存也会被禁用。其还有但不限于以下的限制:响应码为200的GET或者HEAD响应才能被缓存,报文头中不能含有Authorization、Set-Cookie等。测试工具可以拿Google浏览器和PostMan来测试。
  3. 内存缓存
    鉴于服务器端缓存的弊端,故引出了内存缓存,内存缓存的数据是直接保存在当前运行网站程序的内存中,是和进程相关的。当然如果集群数量过多就要考虑分布式缓存了,服务器A、B、C等同时向数据库服务器要数据,依旧会把数据库压垮,下面再讨论分布式缓存的使用。内存缓存的使用,启用AddMemoryCache()服务,使用GetOrCreateAsync来创建缓存值。(这里有缓存过期策略一说,绝对过期时间、滑动过期时间、混合过期时间),直接上代码:
    namespace AlbertZhao.cn.Controllers
    

{
[Route("api/[controller]/[action]")]
[ApiController]
public class DataManagerController : ControllerBase
{
private readonly IMemoryCache memoryCache;
private readonly ILogger logger;

    public DataManagerController(IMemoryCache memoryCache, ILogger logger)
    {
        this.memoryCache = memoryCache;
        this.logger = logger;
    }

    [HttpGet]
    public async Task> GetStuByID(int id)
    {
        logger.LogInformation("开始查询数据....");

        //GetOrCreateAsync天然避免缓存穿透,里面会将空值作为一个有效值
        Student? stu = await memoryCache.GetOrCreateAsync("Student_" + id, async e =>
        {
            //避免缓存雪崩
            e.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(Random.Shared.Next(5, 10));

            //从数据库拿取数据
            using (var ctx = new AlbertDbContext())
            {
                logger.LogInformation("从数据库查询数据....");
                Student? stu = ctx.Students.Where(e=>e.ID == id).FirstOrDefault();
                logger.LogInformation($"从数据库查询的结果是:{(stu == null ? "null" : stu)}");
                return stu;
            } 
        });
        if(stu == null)
        {
            return NotFound("查询的学生不存在");
        }
        else
        {
            return stu;
        }
    }     
}
4. 分布式缓存
如果没有必要就用内存缓存,内存缓存已经很好很好了,只有当分布式设备集群服务器数量很多而数据库只有一台的情况下可以考虑分布式缓存。常用的分布式缓存服务器有Redis、Memcached等。Memcached是缓存专用,性能非常高,但是集群、高可用等方面比较弱且有缓存键的最大长度为256字节的限制,如何使用,请自行查阅EnyimMemcachedCore这个第三方包。笔者采用的是Redis,其做缓存服务器性能比Memcached性能稍差,但是高可用、集群,特别适合在数据量大、高可用等场合使用。关于Redis的使用在附录中做介绍。在ASP.NET Core中引用官方包Microsoft.Extensions.Caching.StackExchangeRedis,启用分布式缓存服务,配置Configuration(这个ConnectionString本地为localhost,也可以写成ip:port)。注入IDistributedCache,通过distributedCache.GetStringAsync尝试从Redis中获取缓存结果,如果缓存结果为空则从数据库中获取,获取完成后放入到Redis缓存中SetStringAsync中。由于Redis中可以存储null,所以缓存穿透的问题不存在,在SetStringAsync中有个参数可以设置缓存过期时间,用Random.Shared.Next来设置随机过期时间避免缓存雪崩的发生。

//启用分布式缓存Redis
builder.Services.AddStackExchangeRedisCache(options => {
options.Configuration = "localhost";
options.InstanceName = "albertzhaoz_";
});

using AlbertZhao.cn.DbContextExtension;
using AlbertZhao.cn.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Caching.Distributed;
using System.Text.Json;
using System.Linq;

namespace AlbertZhao.cn.Controllers
{
[Route("api/[controller]/[action]")]
[ApiController]
public class DataManagerController : ControllerBase
{
private readonly ILogger logger;
private readonly IDistributedCache distributedCache;
public DataManagerController(IMemoryCache memoryCache, ILogger logger, IDistributedCache distributedCache)
{
this.memoryCache = memoryCache;
this.logger = logger;
this.distributedCache = distributedCache;
}

    [HttpGet]
    public async Task> GetStuRedisById(int id)
    {
        logger.LogInformation(id.ToString());
        Student? student = null;
        var s = await distributedCache.GetStringAsync("albertzhaoz" + id);
        if (s == null)
        {
            logger.LogInformation("从数据库中获取");
            student = new AlbertContentDB().GetStuByID(id);              
            logger.LogInformation(JsonSerializer.Serialize(student));
            await distributedCache.SetStringAsync("albertzhaoz" + id, JsonSerializer.Serialize(student),
            new DistributedCacheEntryOptions()
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(Random.Shared.Next(5, 10))
            });
        }
        else
        {
            logger.LogInformation("从Redis缓存中获取");
            student = JsonSerializer.Deserialize(s);
        }

        if (student == null)
        {
            return NotFound("没有此学生数据");
        }
        else
        {
            return student;
        }
    }
}

# 附录-Redis安装及使用
## Windows
按照网络教程安装redis(下载高一点的版本,低版本不支持.NET6),笔者用的可视化工具为AnotherRedisDesktopManager(https://github.com/qishibo/AnotherRedisDesktopManager/releases)命令行一样用,可视化工具直观一点。

## Mac
- Mac下面安装比较简单,直接brew install redis(安装前你也可以先搜索一下brew search redis)brew是什么?HomeBrew请查阅相关资料了解。 
- 启用redis服务redis-server  
- 命令行操作进入到redis-cli
- 查看所有键值keys *
- 查看hash的键值hvals key