关于 ASP.NET 内存缓存你需要知道的 10 点
OSC 协作翻译
英文原文:10 Things To Know About In-Memory Caching In ASP.NET Core
链接:
http://www.binaryintellect.net/articles/a7d9edfd-1f86-45f8-a668-64cc86d8e248.aspx?utm_source=tuicool&utm_medium=referral
译者:leoxu, Tocy, 无若
缓存机制的主要目的是提高应用程序的性能。作为 ASP.NET 开发人员,你可能会意识到 ASP.NET Web 窗体以及 ASP.NET MVC 可以使用 Cache 对象缓存应用程序的数据。这通常被称为服务器端数据缓存,并且常作为框架的内置功能。虽然 ASP.NET Core 中并没有这样的 Cache 对象,但是你可以很容易地实现内存缓存。本文将向你说明如何实现。
在进一步阅读之前,你先创建一个基于 Web 应用程序项目模板的新的 ASP.NET Core 应用程序。
然后按照下面提到的步骤逐一构建和测试由内存缓存提供的各种功能。
1. 内存缓存需要在启动类 Startup 中启用一下
不同于 ASP.NET Web 窗体和 ASP.NET MVC,ASP.NET Core 没有内置的 Cache 对象,可以拿来在控制器里面直接使用。 这里,内存缓存时通过依赖注入来启用的,因此第一步就是在 Startup 类中注册内存缓存的服务。如此,就得打开 Startup 类然后定位到 ConfigureServices() 方法,像下面这样修改 ConfigureServices() 方法:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddMemoryCache();
}
为了向你的应用程序加入内存缓存能力,你需要在服务集合上调用 AddMemoryCache() 方法。采用这种办法就可以让一个内存缓存(它是一个 IMemoryCache 对象)的默认实现可以被注入到控制器中去。
2. 内存缓存使用依赖注入来注入缓存对象
然后打开 HomeController 并对其进行修改,如下所示:
public class HomeController : Controller
{
private IMemoryCache cache;
public HomeController(IMemoryCache cache)
{
this.cache = cache;
}
....
}
如你所见,上述代码声明了一个 ImemoryCache 的私有变量。该变量会被构造器中被赋值。构造器会通过 DI(依赖注入)接收到缓存参数,然后被存储在本地变量总,提供后续使用。
3. 你可以使用 Set() 方法来在缓存中存东西
等你有了这个 IMemoryCache 对象,就可以读取或者向它写入数据了。向缓存写入数据项是相当直接的。
public IActionResult Index()
{
cache.Set<string>("timestamp", DateTime.Now.ToString());
return View();
}
上述代码在 Index() 这个 action 中设置了一个缓存项。这是通过使用 IMemoryCache 的 Set<T>() 来完成的。Set() 方法的第一个参数是键名,用来标识该数据项。第二个参数是键的取值。在此例中,我们存储一个字符串的键和一个字符串的值,而你也可以存储其它类型 (原生以及自定义的类型) 的键值对。
4. 你可以使用 Get 方法来从缓存中获取到一个数据项
等你向缓存中添加好了数据,也许会想要在应用程序的其它地方去获取到该数据,可以用 Get() 来做到。如下代码会告诉你如何来做这件事情。
public IActionResult Show()
{
string timestamp = cache.Get<string>("timestamp");
return View("Show",timestamp);
}
上述代码从 HomeController 的另外一个action(Show)那里获取到了一个缓存的数据项。Get() 方法会指定数据项的类型以及它的键名。如果该数据项存在的话,就会被返回并且被赋值给 timestamp 这个字符串变量。然后这个 timestamp 的值就会被传递给 Show 视图。
Show 视图只是简单地输出了 timestamp 的值,如下所示:
<h1>TimeStamp : @Model</h1>
<h2>@Html.ActionLink("Go back", "Index", "Home")</h2>
为了对目前为止你所写的代码进行一下测试,请运行应用程序。首先将浏览器导航至 /Home/Index ,这样 timestamp 键就会被赋值。然后导航至 /Home/Show 并查看 timestamp 值是否会输出。下图所示是 Show() 这个 action 运行起来的一个例子。
5. 你可以使用 TryGet() 来检查缓存中是否存在特定的键值
如果你观察前面的示例,会发现每次你导航至 /Home/Index 的时候, 都会有一个新的 timestamp 被赋值给了缓存项。这是因为我们并没有对此进行检查,规定只有在数据项不存在的时候才赋值。许多时候你都会想要这样做的。这里有两种办法可以在 Index() 这个 action 里面来做这样的检查。我们把两种办法都在下面列了出来。
//first way
if (string.IsNullOrEmpty
(cache.Get<string>("timestamp")))
{
cache.Set<string>("timestamp", DateTime.Now.ToString());
}
//second way
if (!cache.TryGetValue<string>
("timestamp", out string timestamp))
{
cache.Set<string>("timestamp", DateTime.Now.ToString());
}
第一种办法使用了你早先用过的同一个 Get() 方法,这一次它被拿来跟 if 块一起用。如果 Get() 不能在缓存中找到指定的数据项,IsNullOrEmpty() 就会返回 true。而只有这时候 Set() 才会被调用,一次来添加数据项。
第二种办法更加优雅一点。它使用 TryGet() 方法来获取一个数据项。TryGet() 方法会返回一个布尔值来指明数据项有没有被找到。实际的数据项可以使用一个输出参数拉取出来。如果 TryGet() 返回false,Set() 就会被用来添加数据。
6. 如果不存在的话,可以使用 GetOrCreate() 来添加一项
有时你需要从缓存中检索现有项。如果该项目不存在,则希望添加该项。这两个任务 - 如果它存在获取值,否则创建之 - 可以使用 GetOrCreate() 方法来实现。修改后的 Show() 方法展示了如何实现的。
public IActionResult Show()
{
string timestamp = cache.GetOrCreate<string>
("timestamp", entry => {
return DateTime.Now.ToString(); });
return View("Show",timestamp);
}
Show() 动作现在使用 GetOrCreate() 方法。 GetOrCreate() 方法将检查时间戳的键值是否存在。如果是,现有值将被赋值给局部变量。否则,将根据第二个参数中指定的逻辑创建一个新条目并将其添加到缓存中。
为了测试此代码,请直接运行 /Home/Show,不需要跳转到 /Home/Index。你仍然会看到输出的时间戳值,因为在该值不存在的情况下,GetOrCreate() 现在是添加了它。
7. 你可以在一个缓存的数据项上面设置绝对和滚动的过期时间
在前述示例中,一个缓存项只要被添加到缓存就会一直存储,除非它被明确地使用 Remove() 从缓存中移除。你也可以在一个缓存项上面设置一个绝对和滚动的过期时间。一个绝对的过期设置意味着该缓存项会在严格指定的日期和时间点被移除,而滚动过期设置则意味着它在给定的一段时间量处于空闲状态(也就是没人去访问)之后被移除。
为了能在一个缓存项上面设置这两种过期策略,你要用到 MemoryCacheEntryOptions 对象。如下代码向你展示了如何去使用。
MemoryCacheEntryOptions options =
new MemoryCacheEntryOptions();
options.AbsoluteExpiration =
DateTime.Now.AddMinutes(1);
options.SlidingExpiration =
TimeSpan.FromMinutes(1);
cache.Set<string>("timestamp",
DateTime.Now.ToString(), options);
上述代码来自于修改过的 Index() action,它创建了一个 MemoryCacheEntryOptions 的对象,然后将它的 AbsoluteExpiration 属性设置为从此刻到一分钟之后的一个 DateTime 值,它还将 SlidingExpiration 属性设置为一分钟。这些值都指定了该缓存项会在一分钟之后从缓存移除,不管其是否会被访问。此外,如果该缓存项如初持续空闲了有一分钟,它也会被从缓存中移除。
等你将 AbsoluteExpiration 和 SlidingExpiration 的值设置后, Set() 方法就可以被用来将一个数据项添加到缓存。这一次 MemoryCacheEntryOptions 对象会被作为第三个参数传递给 Set() 方法。
8. 当缓存项会被移除时,你可以连接回调
有时你会想要在缓存项从缓存中被移除时收到通知。可能会有多种原因需要从缓存中移除数据项。例如,因为明确地执行了 Remove() 方法而移除了一个缓存项, 也有可能是因为它的 AbsoluteExpiration 和 SlidingExpiration 值已经到期而被移除,诸如此类的原因。
为了能知道项目是何时从缓存移除的,你需要编写一个缓存函数。如下代码向你展示了如何去做这件事情:
MemoryCacheEntryOptions options = new MemoryCacheEntryOptions();
options.AbsoluteExpiration =
DateTime.Now.AddMinutes(1);
options.SlidingExpiration =
TimeSpan.FromMinutes(1);
options.RegisterPostEvictionCallback
(MyCallback, this);
cache.Set<string>("timestamp",
DateTime.Now.ToString(), options);
上述代码同之前使用 MemoryCacheEntryOptions 来配置 AbsoluteExpiration 和 SlidingExpiration 的代码相当类似。更加重要的是它也调用了 RegisterPostEvictionCallback() 方法来绑定刚刚讨论过的回调函数。在这里回调函数被命名为 MyCallback。第二个参数是一个你会想要传递给回调函数的状态对象。这里我们传入了 HomeController 的实例 (用 this 将当前的 HomeController 对象“点”出来) 作为状态对象。
前面提到的MyCallback函数,其代码如下所示:
private static void MyCallback(object key, object value,
EvictionReason reason, object state)
{
var message = $"Cache entry was removed : {reason}";
((HomeController)state).
cache.Set("callbackMessage", message);
}
请仔细观察这段代码。 MyCallback() 是 HomeController 类里面的一个私有静态函数,它有四个参数。前面两个参数表示刚刚删除的缓存项的键和值,第三个参数表示的是该数据项被删除的原因。EvictionReason 是一个枚举类型,它维护者各种可能的删除原因,如过期,删除以及替换。
在回调函数的内部,我们会基于删除的原因构造一个字符串消息。我们想要将此消息设置成另外一个缓存项。这样做的话就需要访问 HomeController 的缓存对象,此时状态参数就可以排上用场了。使用状态对象,你可以对 HomeController 的缓存对象进行控制,并使用 Set() 增加一个 callbackMessage 缓存项。
你可以通过 Show() 这个 action 来访问到 callbackMessage,如下所示:
public IActionResult Show(){
string timestamp = cache.Get<string>("timestamp");
ViewData["callbackMessage"] =
cache.Get<string>("callbackMessage");
return View("Show",timestamp);
}
最后就可以在 Show 视图中显示出来了:
<h1>TimeStamp : @Model</h1>
<h3>@ViewData["callbackMessage"]</h3>
<h2>@Html.ActionLink("Go back", "Index", "Home")</h2>
为了测试回调,我们需要运行应用程序并跳转到 /Home/Index。然后跳转到 /Home/Show,并不停地刷新浏览器。在某些时间点,由于其 AbsoluteExpiration 设置之后,时间戳项目将会过期。你会看到这样的 callbackMessage:
9. 你可以设置缓存项的优先级
正如你可以设置缓存项的到期策略一样,你还可以为缓存项赋予优先级。如果服务器内存紧缺的话,就会基于此优先级对缓存项进行清理以回收内存。 想要设置优先级的话,就要再一次用到 MemoryCacheEntryOptions。
MemoryCacheEntryOptions options =
new MemoryCacheEntryOptions();
options.Priority = CacheItemPriority.Normal;
cache.Set<string>("timestamp",
DateTime.Now.ToString(), options);
MemoryCacheEntryOptions 的 Priority 属性让你可以使用 CacheItemPriority 枚举来设置缓存项的优先级取值。可选的值有 Low,Normal,High 以及 NeverRemove。
10. 你可以设置多个缓存项之间的依赖关系
你还可以对一组缓存项目之间的依赖关系进行设置,例如在删除一个缓存项时,所有依赖的项也会被删除。 要是你想要了解它是如何工作的,可以像下面这样对 Index()这个 action 做一下修改:
public IActionResult Index()
{
var cts = new CancellationTokenSource();
cache.Set("cts", cts);
MemoryCacheEntryOptions options =
new MemoryCacheEntryOptions();
options.AddExpirationToken(
new CancellationChangeToken(cts.Token));
options.RegisterPostEvictionCallback
(MyCallback, this);
cache.Set<string>("timestamp",
DateTime.Now.ToString(), options);
cache.Set<string>("key1", "Hello World!",
new CancellationChangeToken(cts.Token));
cache.Set<string>("key2", "Hello Universe!",
new CancellationChangeToken(cts.Token));
return View();
}
代码首先创建了一个 CancellationTokenSource 对象,该对象被存储为一个独立的缓存项 cts。然后像之前那样创建出 MemoryCacheEntryOptions 对象。这时候调用 MemoryCacheEntryOptions 的 AddExpirationToken() 方法来指定过期令牌。我们不会在这里探讨 CancellationChangeToken 的细节。可以这样理解,过期令牌能让你有权利让一个缓存项过期。如果令牌处于活动状态的话,则缓存项就会在缓存中维持,而如果令牌被取消掉了,则该缓存项就将从缓存中删除掉。一旦缓存项从缓存中删除掉了,MyCallback 就像之前一样被调用。之后代码又创建了两个缓存项—— key1 和 key2。在添加这两个缓存项时,Set() 的第三个参数将基于之前所创建的 cts 对象传递一个 CancellationChangeToken。
这样做就意味着这里我们有了三个键 - timestamp 是主键,而 key1 和 key2 则依赖于 timestamp。当 timestamp 被删除时,key1 和 key2 也应该被删除掉。要删除 timestamp,你需要在代码中的某个地方取消其令牌。我们可以单独的一个 action(Remove())中进行这样的操作。
public IActionResult Remove()
{
CancellationTokenSource cts =
cache.Get<CancellationTokenSource>("cts");
cts.Cancel();
return RedirectToAction("Show");
}
这里我们先获取到之前存储的 CancellationTokenSource 对象,并调用它的 Cancel() 方法。这样做会把 timestamp,key1 以及 key2 都删除掉。 你可以通过在 Show() 这个 action 中获取一下所有这三个键来确认它们是否已经被删除掉了。
为了测试这个例子,运行应用程序并导航至 /Home/Index。然后再导航至 /Home/Show,并检查所有这三个键值是否按预期显示了出来。然后导航至 /Home/ Remove,浏览器将被重定向回 /Home/Show。由于 Remove() 取消了令牌,所有的键都已经被删除调了,而现在 Show 视图会将删除的原因(TokenExpired)显示出来,如下所示:
到目前为止就是这些了!笔耕不辍!