ASP.NET Core缓存

基础知识

Caching通过减少生成内容所需的工作,显著提高应用的性能和可伸缩性。 Caching最适用于不经常更改且 生成成本高 的数据。 Caching可比从源返回快得多的数据副本。

ASP.NET Core支持多个不同的缓存

缓存准则

  • 代码应始终具有用于提取数据的选项,而不是依赖于可用的缓存值。
  • 缓存的是内存资源,内存资源是有限的因此需要限制缓存增长:
    • 请勿将外部输入用作缓存Key,如果任由外部输入作为Key,那缓存很容易就会被恶意刷爆。
    • 使用过期时间策略限制缓存增长,这能够及时释放不活跃的缓存,及时释放内存空间。
    • 使用 SetSize、Size 和 SizeLimit 限制缓存大小。 ASP.NET Core运行时 不会根据 内存压力限制缓存大小。 由开发人员限制缓存大小。

客户端缓存 Cache-control

Http协议中规定,服务器端通过返回报文头添加Cache-control,来达到通知客户端进行缓存,如:Cache-control:max-age=30,表示让客户端缓存该内容30秒(当然客户端也可以不干)。

在ASP.NET Core中,可以通过添加ResponseCacheAttribute这个Attribute,让程序在返回时的添加报文头Cache-control

1
2
3
4
5
6
7
8
9
public class ValuesController : Controller
{
[HttpGet]
[ResponseCache(Duration = 30)]
public ActionResult<bool> Get(int accId)
{
return accountService.Validate(accID);
}
}

客户端缓存只对本客户端有效,在你使用另一个客户端发出一样的请求是,你就会发现访问的不是缓存了的数据,而是最新数据。这时候就需要使用服务端缓存了。

服务器端响应缓存 ResponseCaching

ASP.NET Core不但可以设置浏览器缓存,还可以设置服务器端的响应缓存,当请求某资源时,响应的内容会被服务器进行缓存,在缓存有效期内,就算不同客户端获取同一资源也不会真的进入该资源地址获取数据,而是由缓存直接返回内容。

使用ResponseCaching

要使用服务器缓存,需要以下步骤:

  1. Startup.ConfigureServices中添加:

    1
    services.AddResponseCaching();
  2. Startup.Configure中添加:

    1
    app.UseResponseCaching();
  3. 同样需要添加ResponseCacheAttribute这个Attribute:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class ValuesController : Controller
    {
    [HttpGet]
    [ResponseCache(Duration = 30)]
    public ActionResult<bool> Get(int accId)
    {
    return accountService.Validate(accID);
    }
    }

这时候你通过同一客户端多次访问该资源发现第二次以后的访问的是客户端缓存,通过不同客户端访问的是服务器缓存的内容。

上面的例子用的是 MemoryCache 作为储存介质,如果应用重启了,缓存的数据就会失效。

ResponseCaching的缓存条件

  • 请求必须生成状态代码为”正常”的 200 (服务器) 响应。
  • 请求方法必须是 GET 或 HEAD。
  • Startup.Configure 中,Caching中间件的中间件之前必须放置需要缓存的中间件。 有关详细信息,请参阅 ASP.NET Core 中间件
  • Authorization标头不能存在。
  • Cache-Control 标头参数必须有效,并且响应必须标记 public 且未标记为 private
  • 如果标头不存在,则标头不得存在,因为标头 Pragma: no-cache Cache-Control 将替代标头( Cache-Control Pragma 如果存在)。
  • Set-Cookie标头不能存在。
  • Vary 标头参数必须有效且不等于 *
  • 如果 Content-Length 设置了 (标头值) 必须与响应正文的大小匹配。
  • IHttpSendFileFeature未使用 。
  • 响应不能像 标头和 和 缓存 Expires 指令所指定 max-ages-maxage 样过时。
  • 响应缓冲必须成功。 响应的大小必须小于配置的 或默认的 SizeLimit 。 响应的正文大小必须小于配置的 或默认的 MaximumBodySize
  • 响应必须可缓存,符合 RFC 7234 规范。 例如,指令 no-store 不得存在于请求或响应标头字段中。 有关详细信息 ,请参阅第 3 部分:在 RFC 7234 的缓存中存储响应。

ResponseCaching只是看上去美好

虽然服务器端响应缓存看起来很美好,不过实际上有点中看不中用,应为其生效是有条件的,而且还挺苛刻,实际上只要请求的报文头加上Cache-Control:no-cache,就可以让服务器响应缓存失效。所以一般我们更加依赖于:内存缓存分布式缓存

内存缓存 IMemoryCache

内存缓存是数据保存在当前运行的网站的内存中,是与进程相关的。而由于在Web服务器中,多个不同网站时运行在不同进程中的,因此不同网站的内存时不会互相干扰的,也就意味着不能直接访问不在同一进程中的其他应用程序的内存缓存,然后需要注意的是网站重启后,内存缓存中的数据会清空。

使用内存缓存

要使用内存缓存,需要:

  1. Startup.ConfigureServices中添加:

    1
    services.AddMemoryCache();
  2. 注入IMemoryCache

    1
    2
    3
    4
    5
    private IMemoryCache _cache;
    public MemoryCacheController(IMemoryCache memoryCache)
    {
    _cache = memoryCache;
    }
  3. 使用TryGetValueRemoveSetGetOrCreate等方法操作MemoryCache

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    [Route("api/[controller]")]
    [ApiController]
    public class MemoryCacheController : ControllerBase
    {
    private IMemoryCache _cache;
    public MemoryCacheController(IMemoryCache memoryCache)
    {
    _cache = memoryCache;
    }

    [HttpGet]
    public string Get()
    {
    string serverTime;

    #region 读取写入缓存
    //方法一、使用TryGetValue与Set
    //if (!_cache.TryGetValue("servertime", out serverTime))
    //{
    // serverTime = DateTime.Now.ToString("F");
    // _cache.Set("servertime", serverTime);
    //}
    //方法二、使用GetOrCreate
    //serverTime = _cache.GetOrCreate<string>("serverTime", (e) => { return DateTime.Now.ToString("F");});
    #endregion

    return serverTime;
    }

    [HttpGet("Remove")]
    public void Remove(string cacheName)
    {
    _cache.Remove(cacheName);
    }
    }

访问Get方法获得数据并设置一个名字为 **servertime **的MemoryCache,在过期前再次访问获得的都是MemoryCache里的数据,可以通过访问Remove方法,传入参数”servertime “,即可清除名字为”servertime “的MemoryCache。

过期时间策略

默认情况下内存缓存是不会过期的,因此我们可以通过Remove方法删除缓存,或者使用Set方法重新设置缓存,不过在实际应用中大部分时间不会这么做,因为这么弄很麻烦,更常用的办法时使用过期时间来管理。

过期时间策略有两种

  • 绝对过期时间,通过设置ICacheEntry.AbsoluteExpirationRelativeToNow(过多少时间后失效)或ICacheEntry.AbsoluteExpiration(什么时间点失效)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    //不会过期
    serverTime = _cache.GetOrCreate<string>("serverTime", (e) => { return DateTime.Now.ToString("F"); });

    //5秒后过期
    serverTime = _cache.GetOrCreate<string>("servertime", (e) =>
    {
    e.AbsoluteExpirationRelativeToNow = System.TimeSpan.FromSeconds(5);
    return DateTime.Now.ToString("F");
    });

    //在今天晚上23点失效
    serverTime = _cache.GetOrCreate<string>("servertime", (e) =>
    {
    e.AbsoluteExpiration = System.DateTime.Today.AddDays(23);
    return DateTime.Now.ToString("F");
    });
  • 滑动过期时间,通过设置ICacheEntry.SlidingExpiration(过多少时间后失效,但是如果在失效前一直有访问尽量,那就会延长设置的时间(注意是在被访问的时间点上),直到过期。

    1
    2
    3
    4
    5
    6
    //5秒后过期,假如在过期前被访问,以被访问时间点位基准再次延迟5秒,直到超过5秒无访问过期
    serverTime = _cache.GetOrCreate<string>("servertime", (e) =>
    {
    e.SlidingExpiration = System.TimeSpan.FromSeconds(5);
    return DateTime.Now.ToString("F");
    });
  • 不过可以混用两种策略,让缓存数据能够在滑动过期时间的基础上,通过设置绝对过期时间来达到强制刷新数据的目的,也就是说在绝对过期时间这个点数据必定过期,滑动时间影响不了。

    1
    2
    3
    4
    5
    6
    7
    //混用绝对过期时间与滑动过期时间
    serverTime = _cache.GetOrCreate("servertime", (e) =>
    {
    e.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(15); //绝对过期时间15秒
    e.SlidingExpiration = TimeSpan.FromSeconds(5); //滑动过期时间5秒
    return DateTime.Now.ToString("F");
    });

分布式缓存 DistributedCache

内存缓存在比较简单的应用里是足够使用的,但是对于现在需要多台服务器进行负载均衡的架构环境中就显得有点不太够用,如会出现:缓存内容不一致、多台服务器缓存同一内容浪费内存空间等问题,因此需要一个独立的统一的缓存存储中心,以便让所有的Web服务器共享同一份缓存数据,我们将这种缓存形式称为分布式缓存

有关.NET Core的分布式缓存

  • .NET Core没有内置分布式缓存,但是却提供了统一的分布式缓存服务器的操作接口IDistributedCache,用法实际与内存缓存类似。

  • 可以使用常用分布式缓存如RedisNCacheSQL ServerMemcached等。

    SQL Server:缓存性能并不好,基本不考虑。

    Memcached:缓存专用,性能非常高能,可惜对集群、高可用方面支持比较弱,而且有缓存键最大长度为250字节(以前做项目刚用时就被坑过,超过了就直接截断,也没报错提醒)、最大只能存储1MB的单个item等限制。

    Redis:虽然做缓存服务器的性能比Memcached稍差,但是其高可用、集群等方面非常强大,适合在数据量大及集群等场景使用,并且缓存外的功能也很强大,还能做消息队列等。

    NCache:它是一个高性能的、分布式的、可扩展的、天生为.Net设计的缓存框架,NCache不仅比 Redis 快,而且还提供了一些Redis所不具有的分布式特性。

  • 分布式缓存其缓存值的类型为byte[],虽然也提供了接受string的类型的方法,但是最终都会转换为byte[],而且本身IDistributedCache提供的方法有限,最好扩展IDistributedCache的方法。

IDistributedCache接口包含以下方法

  • Get、GetAsync

    采用字符串键并以byte[]形式检索缓存项(如果在缓存中找到)。

  • Set、SetAsync

    使用字符串键向缓存添加项byte[]形式)。

  • Refresh、RefreshAsync

    根据键刷新缓存中的项,并重置其可调过期超时值(如果有)。

  • Remove、RemoveAsync

    根据键删除缓存项。

Redis 分布式缓存

  1. 安装Redis相关包,这个包是微软提供的

    1
    2
    install-package Microsoft.Extensions.Caching.StackExchangeRedis
    //注意以前使用的是:Microsoft.Extensions.Caching.Redis包,不过这个包2018年后就没更新了
  2. Startup.ConfigureServices中注册

    1
    2
    3
    4
    5
    //Use Redis
    services.AddStackExchangeRedisCache(options => {
    options.Configuration = "localhost"; //使用本地
    options.InstanceName = "SampleInstance_"; //添加前缀可以区别于其他应用来的数据
    });
  3. 注入

    1
    2
    3
    4
    5
    private IDistributedCache _cache;
    public RedisController(IDistributedCache cache)
    {
    _cache = cache;
    }
  4. 使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    [Route("api/[controller]")]
    [ApiController]
    public class RedisController : ControllerBase
    {
    private IDistributedCache _cache;
    public RedisController(IDistributedCache cache)
    {
    _cache = cache;
    }

    [HttpGet("Get")]
    public string Get()
    {
    //获取
    var obj = _cache.Get("id1");
    return (obj == null ? "Err:Not Found!" : Encoding.Default.GetString(obj));
    }

    [HttpGet("Set")]
    public string Set()
    {
    var obj = _cache.Get("id1");
    if (obj == null)
    {
    int absoluteExpirationRelativeToNowSec = 100;
    int slidingExpirationSec = 50;

    //添加,注意:添加Redis后的类型是hash
    //默认没有过期时间
    //_cache.Set("id1", Encoding.Default.GetBytes("永不过期,缓存时间是:{DateTime.Now.ToLongTimeString()}"), new DistributedCacheEntryOptions { });
    //同样可以设置绝对过期时间与滑动过期时间
    _cache.Set(
    "id1",
    Encoding.Default.GetBytes($"缓存时间是:{DateTime.Now.ToLongTimeString()},过期时间是:{DateTime.Now.AddSeconds(absoluteExpirationRelativeToNowSec).ToLongTimeString()}"),
    new DistributedCacheEntryOptions
    {
    AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(absoluteExpirationRelativeToNowSec),
    SlidingExpiration = TimeSpan.FromSeconds(slidingExpirationSec)
    }
    );
    }

    obj = _cache.Get("id1");
    return (obj == null ? "Err:Not Found!" : Encoding.Default.GetString(obj));
    }

    [HttpGet("Del")]
    public void Del()
    {
    //移除
    _cache.Remove("id1");
    }

    [HttpGet("Refresh")]
    public string Refresh()
    {
    //刷新,Get命令同样会刷新滑动过期时间
    _cache.Refresh("id1");

    var obj = _cache.Get("id1");
    return (obj == null ? "Err:Not Found!" : Encoding.Default.GetString(obj));
    }
    }

缓存问题

缓存穿透

访问一个缓存和数据库都不存在的key,由于该key注定获得不了数据,因此不会被写缓存,永远都会直接访问到数据库上。这种缓存永远起不了作用,直接被”穿透“到数据库的情况,被称为缓存穿透。

解决方案

  • 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
  • 当出现这种请求时,也将其缓存,其key为请求的key,value为null,但是缓存有效时间设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击。

缓存击穿

某一个热点key,在缓存过期的一瞬间,同时有大量的请求打进来,由于此时缓存过期了,所以请求最终都会走到数据库,造成瞬时数据库请求量大、压力骤增,甚至可能打垮数据库。

解决方案

  • 加互斥锁。在并发的多个请求中,只有第一个请求线程能拿到锁并执行数据库查询操作,其他的线程拿不到锁就阻塞等着,等到第一个线程将数据写入缓存后,直接走缓存。
  • 热点数据不过期。直接将缓存设置为不过期,然后由定时任务去异步加载数据,更新缓存。

缓存雪崩

缓存雪崩是由于原有缓存失效(过期),新缓存未到期间。所有请求都去查询数据库,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列连锁反应,造成整个系统崩溃。

解决方案

  • 加互斥锁。同缓存击穿的。

  • 热点数据不过期。同缓存击穿的。

  • 错开过期时间。在基础过期时间的基础上,加上一个随机时间,防止同一时间大量数据过期现象发生。如:本来统一设置位3分钟的,变为3分钟加1-15秒的随机数。

注意

本文章是基本上是基于ASP.NET Core3.1版本编写,其后续版本可能发生变化,具体使用请去微软官方文档查看。

参考

微软官方文档

.NET 6教程,.Net Core 2022视频教程,杨中科主讲

缓存穿透、缓存击穿、缓存雪崩解决方案