缓存Cache:API / Output

引子:如果有一把枪指着我的头,要我在5分钟内解决项目的性能问题,我立马就会开始缓存……

最简单的性能提升方式:缓存。

背景知识

英文cache,在台湾被翻译成“快取”,个人认为,更符合它的特点:一种快速获取数据的技术。

为什么能够_快取_?

  • 存储位置不同:比如本地比服务器更快,内存比磁盘更快
  • 不用计算:把上一次的计算结果直接存放起来,下次直接用

所以,简单的说,缓存就是:

  1. 把耗费大量资源(比如通过数据库检索)或/和时间(比如浏览器通过网络)获取的数据,存放在一个能够快速获取的地方(缓存区),
  2. 这样下次就可以直接从缓存区中快速的获取

它是一种提供系统性能的常用方法,被广泛的使用在CPU_(复习)_、数据库、浏览器(_复习:image TagHelper_)……

流程图示

具体来说,通过缓存获取数据(并同时将新获取数据存入缓存)的过程如下所示:
缓存Cache:API / Output_第1张图片

  1. 先试着从缓存中获取数据,缓存中
  2. 有:直接返回
  3. 没有:从数据库中获取到数据
  4. 将数据存入缓存
  5. 将数据返回给客户

Cache对象

MVC中可以HttpContext直接获取缓存对象,可以把它当做一个“全局”容器,里面以键(string)值(object)对的形式存储着缓存数据,可调用其索引器读写缓存数据。

@想一想@:应该如何用代码实现一个利用缓存的过程?

//首先需要一个key string cacheKey = "user_atai"; //先试着从Cache中取 IndexModel model = HttpContext.Cache[cacheKey] as IndexModel; //取不到 if (model == null) { //从数据库取 model = service.GetBy("阿泰"); //别忘了存入cache HttpContext.Cache[cacheKey]= model; }

实际上,上述索引器内部只是简单的调用了两个方法:

  • Get(key)方法:获取缓存数据
  • Insert(key, value)方法:添加缓存数据

而Insert()方法还有很多重载,可以定义更多的缓存配置:

依赖(Dependency)

默认的cache数据会一直保存,这样,如果真实/源数据发生改变,缓存数据就会变得“不正确”。

演示:更改数据库中数据,改变页面呈现……

所以,ASP.NET提供了缓存依赖机制,即:为缓存数据设置一个“依赖”,如果依赖发生变化,让缓存失效,以便能获取正确数据。

我们常用的是

SqlCacheDependency

即:一旦数据库(上的表)发生变动,就让缓存失效。

因为localDb只不支持从数据库发送notification(通知)到.NET运行时,所以我们采用的是poll机制:由ASP.NET定时的轮循检查数据库变更。

所以需要在数据库上设置一个表,专门的记录其最后更改时间……

首先,使用aspnet_regsql.exe命令配置数据库:

  1. 打开VS的命名行工具:Developer Command Prompt
  2. 在弹出的黑窗口中输入命令行:

    aspnet_regsql.exe -S (localdb)MSSQLLocalDB -E -d 17bang -ed

    启动数据库的缓存依赖,其中:

    • -S 后跟数据库服务器对象名 (localdb)MSSQLLocalDB
    • -d 后跟数据库名 17bang

    检查数据库会发现多了一个表:AspNet_SqlCacheTablesForChangeNotification

  3. 再输入命令:

    aspnet_regsql.exe -S (localdb)MSSQLLocalDB -E -d 17bang -t Users -et

    启动数据库上的表启动缓存依赖,其中:

    • -t 后跟表名Users

    检查数据库发现表AspNet_SqlCacheTablesForChangeNotification里

然后,在web.config的system.web中配置poll时间等

其中pollTime的单位是毫秒。

最后,可以在Insert时添加SqlCacheDependency实例对象做参数:

HttpContext.Cache.Insert(cacheKey, model, //17bang数据库名,Users是表名 new SqlCacheDependency("17bang", "Users"));

演示:改变数据库中的数据……

过期时间(Expire)

其实,实际开发中更常用的是设置缓存区数据的_过期时间_。一旦超过过期时间,就将该缓存数据予以清理。

ASP.NET中可以设置:

  1. 永不过期(NeverRemoved):默认值
  2. 绝对(Absolute)过期时间:即缓存只在某一绝对时间(比如2020年2月11日20:50)以前有效
  3. 滑动(Slide)过期时间:其实就是一段时间,比如30秒,它从缓存数据最后一次被访问起算,30秒后过期;但是,在这30秒内,一旦该数据又被访问,那么过期时间将会被重新计算,变成当前时间加30秒……(和session失效机制非常类似)

在MVC中,绝对/滑动过期时间不能同时设立,否则会报异常(演示:略):

  • 设置Absolute时Slide参数只能为TimeSpan.Zero

    DateTime.Now.AddSeconds(30), TimeSpan.Zero

  • 设置Slide时Absolute参数只能为DateTime.MaxValue

    DateTime.MaxValue, new TimeSpan(0, 0, 5),

@想一想@:为什么要设计一个滑动过期呢?

滑动过期基于这样一种假设:越是被频繁请求的数据,以后就越有可能被再次请求_(类似于“犹太定律”,事实上也确实如此,^_^)_。利用滑动过期,就能自动的筛选出最被频繁访问的数据,提高命中率,最大限度的压榨性能。

优先级(Priority)

和Session类似,虽然我们设置了缓存的过期时间,但并不能保证在此期间缓存一定存在——在某些情况下,有效期内的缓存也会被MVC自动清除。

这时候,ASP.NET会清除那些优先级低的,保留优先级高的。

我们可以在插入缓存数据的时候指定其优先级(枚举):

public enum CacheItemPriority

CacheItemRemovedCallback

MVC还为我们提供了一个delegate参数选项

public delegate void CacheItemRemovedCallback( string key, object value, CacheItemRemovedReason reason);

可以设定当cache数据过期/被删除时可调用的方法。我们来演示一下:

(k, v, r) => {Trace.WriteLine( $"cache with key:({k}) and value:({v}) is deleted, reason:({r})"); }

演示:output窗口输出……

常见面试题:

Add() vs Insert()

当存入的数据键值和缓存中已有的数据键值重复时:

  • Add():不予处理
  • Insert():使用新数据覆盖旧数据

演示:同一个key两次I插入,略

Cache vs Session

  • cache是全局的,所有用户都可以访问的;
  • session是基于用户的,A用户session中是数据B用户无法获得

OutputCache

使用上文所述的API很灵活,但:

  • 只是缓存UI层获取数据,
  • 而且稍显累赘

所以MVC推出了OutputCache,可以:

  • 直接缓存生成的Html数据
  • 可以声明方式实现

    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)] public class OutputCacheAttribute : ActionFilterAttribute, IExceptionFilter

可以看出,OutputCache可适用于Controller和Action,包括ChildAction (@想一想@:为什么?)

常用设置

  • Duration:缓存时间,单位秒。这里只能是absolute的,不能slide
  • Location:可设置为已枚举值,确定缓存数据的存放位置,一般是Server(服务器端)、Client(客户端)和Any(任意位置,默认)。注意:设置客户端仅意味着ASP.NET(通过Response Header)给客户端一个请求缓存的指示,不能保证浏览器一定按指示予以缓存。
  • VaryByParam:指示是否根据url参数设置不同的副本,可以填写的值为:

    *

    为不同的url参数(名称/个数/值)缓存不同的副本,最新MVC版本默认项

    None

    忽略url参数的差异,所有不同url参数都共用同一个缓存副本

    分号(;)分隔的参数名,如:id;name

    为指定的url参数缓存不同的副本,忽略未指定的url参数差异

断点演示:Action中的断点不会被击中……

Cache Profile

可以在web.config的caching中配置:

然后,在OutputCache中引用:

[OutputCache(CacheProfile = "RegisterIndex")] public ActionResult Index(int id = 0)

这样,能实现和

[OutputCache(Duration = 5)]

一样的效果。

@想一想@:为什么还要额外提供这个在web.config中进行配置的CacheProfile选项?

此外,还可以直接禁用OutputCache:

MVCDonutCache

很多时候,我们希望能缓存一个页面,但是其中的某一个部分例外(如:LogonStatus),怎么办呢?

MVC暂时未能提供内置的支持,我们需要(通过NuGet)引入第三方插件MVCDonutCache 。_(演示:略)_

它的实现非常简单:

  1. 将父Action的OutputCache换成DonutOutputCache

    [DonutOutputCache(Duration = 5)] //需要添加using DevTrends.MvcDonutCaching;

  2. 在@Html.ChildAction()末尾添加一个bool值参数true,如:

    @Html.Action("_Inviter", "Shared", true)

演示:父Action页面和ChildAction页面都显示DateTime.Now

使用技巧

知道缓存的代价

缓存的本质:用空间换时间。要明白缓存的代价是“空间”,不能让缓存的内存空间挤压了正常的程序运行。

不应该,或者少缓存,只是短期缓存:

  • 本身不难获取
  • 不会被频繁使用
  • 频繁变动

的数据。

复习:性能问题金句:

  1. 天下没有免费的晚餐
  2. no profile,no improvement
  3. 首先优化瓶颈
  4. 过早的优化是万恶之源

合理使用缓存策略

比如,需要精细化控制的、多个/种页面使用的数据源(文件评论),就适合用编程API的方式,以节省内存空间,避免生成多个View Cache副本(有不同排序方式)

比如,合理的使用ChildAction Cache组合(关键字等Widget/内容列表项),以及DonutCache“挖孔”,都可以提高OutputCache的利用率。

……

当然,关键是profile!利用web.config,针对不同的服务器环境,不断调优!

作业

  1. 思考“一起帮”那些页面内容可以缓存,为什么?找出三个以上的例子,按不同的缓存策略,分别使用:

    • 编程API
    • Action和ChildActio的OutputCache
    • DonutCache缓存之
  2. 缓存内容列表页,但是,当发布一篇新内容时,让之前缓存的按发布时间排列的内容列表页失效。提示:使用RemoveCallback回调参数……

你可能感兴趣的:(firefox)