引子:如果有一把枪指着我的头,要我在5分钟内解决项目的性能问题,我立马就会开始缓存……
最简单的性能提升方式:缓存。
背景知识
英文cache,在台湾被翻译成“快取”,个人认为,更符合它的特点:一种快速获取数据的技术。
为什么能够_快取_?
- 存储位置不同:比如本地比服务器更快,内存比磁盘更快
- 不用计算:把上一次的计算结果直接存放起来,下次直接用
所以,简单的说,缓存就是:
- 把耗费大量资源(比如通过数据库检索)或/和时间(比如浏览器通过网络)获取的数据,存放在一个能够快速获取的地方(缓存区),
- 这样下次就可以直接从缓存区中快速的获取
它是一种提供系统性能的常用方法,被广泛的使用在CPU_(复习)_、数据库、浏览器(_复习:image TagHelper_)……
流程图示
具体来说,通过缓存获取数据(并同时将新获取数据存入缓存)的过程如下所示:
- 先试着从缓存中获取数据,缓存中
- 有:直接返回
- 没有:从数据库中获取到数据
- 将数据存入缓存
- 将数据返回给客户
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命令配置数据库:
- 打开VS的命名行工具:Developer Command Prompt
在弹出的黑窗口中输入命令行:
aspnet_regsql.exe -S (localdb)MSSQLLocalDB -E -d 17bang -ed
启动数据库的缓存依赖,其中:
- -S 后跟数据库服务器对象名 (localdb)MSSQLLocalDB
- -d 后跟数据库名 17bang
检查数据库会发现多了一个表:AspNet_SqlCacheTablesForChangeNotification
再输入命令:
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中可以设置:
- 永不过期(NeverRemoved):默认值
- 绝对(Absolute)过期时间:即缓存只在某一绝对时间(比如2020年2月11日20:50)以前有效
- 滑动(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 。_(演示:略)_
它的实现非常简单:
- 将父Action的OutputCache换成DonutOutputCache
[DonutOutputCache(Duration = 5)] //需要添加using DevTrends.MvcDonutCaching;
- 在@Html.ChildAction()末尾添加一个bool值参数true,如:
@Html.Action("_Inviter", "Shared", true)
演示:父Action页面和ChildAction页面都显示DateTime.Now
使用技巧
知道缓存的代价
缓存的本质:用空间换时间。要明白缓存的代价是“空间”,不能让缓存的内存空间挤压了正常的程序运行。
不应该,或者少缓存,只是短期缓存:
- 本身不难获取
- 不会被频繁使用
- 频繁变动
的数据。
复习:性能问题金句:
- 天下没有免费的晚餐
- no profile,no improvement
- 首先优化瓶颈
- 过早的优化是万恶之源
合理使用缓存策略
比如,需要精细化控制的、多个/种页面使用的数据源(文件评论),就适合用编程API的方式,以节省内存空间,避免生成多个View Cache副本(有不同排序方式)
比如,合理的使用ChildAction Cache组合(关键字等Widget/内容列表项),以及DonutCache“挖孔”,都可以提高OutputCache的利用率。
……
当然,关键是profile!利用web.config,针对不同的服务器环境,不断调优!
作业
思考“一起帮”那些页面内容可以缓存,为什么?找出三个以上的例子,按不同的缓存策略,分别使用:
- 编程API
- Action和ChildActio的OutputCache
- DonutCache缓存之
- 缓存内容列表页,但是,当发布一篇新内容时,让之前缓存的按发布时间排列的内容列表页失效。提示:使用RemoveCallback回调参数……