asp.net core 系列之Reponse caching之cache in-memory (2)

2019-09-11 14:43:50 浏览数 (1)

这篇文章(主要翻译于官网,水平有限,见谅)讲解asp.net core 中的 Cache in-memory (内存缓存).

Cache in-memory in ASP.NET Core

Caching basics

Caching 可以显著的提升应用的performance(表现) 和 scalability,通过减少生成内容所必需的做的工作。Caching 在变动比较的数据上工作的最好。Caching 可以做一个备份数据,使得数据比从原来的地方取的快一些。

ASP.NET Core支持几种不同的缓存。最简单的缓存是基于IMemoryCache, 它代表一个存储在web服务器的内存上的cache(缓存)。当使用in-memory cache时,运行在多个服务器上的服务器集群应该确保sessions是不动的,不动的sessions(Sticky sessions)确保随后的从一个client发来的请求全都到同一台服务器。例如,Azure Web apps用Application Request Routing(ARR)来路由所有随后的请求到同一个服务器。

在一个web集群上的Non-sticky sessions 要求一个distributed cache(分布式缓存)来避免缓存一致性问题。对于一些应用,a distributed cache 可以支持更高的扩展比in-memory cache. 用一个分布式缓存卸载内存缓存到一个外部处理中。

In-memory cache 可以存储任意对象;distributed cache interface 仅限于byte[]. 对于in-memory和distributed cache 存储cache items为key-value pairs.

System.Runtime.Caching/MemoryCache

System.Runtime.Caching/MemoryCache可以被用在:

  • .NET Standard 2.0 or later
  • Any .NET implementation that targets .NET Standard 2.0 or later. 例如, ASP.NET Core 2.0 or later
  • .NET Framework 4.5 or later

Microsoft.Extensions.Caching.Memory/IMemoryCache 被推荐在System.Runtime.Cachign/MemoryCache之上使用, 因为Microsoft.Extensions.Caching.Memory/IMemoryCache是更好的集成在ASP.NET Core中。例如,IMemory 天生可以用ASP.NET Core的依赖注入工作。

用System.Runtime.Caching/MemoryCache作为一个兼容桥梁,当移植代码从ASP.NET 4.X 到ASP.NET Core时。

Cache guidelines
  • 代码应该总有一个可靠的选项来取数据并且不是依赖于缓存的可得到的值
  • 缓存使用稀少的资源,内存。限制缓存增长(cache growth)(内存是稀缺资源, 如果在内存中使用缓存,需要限制缓存增长):
    • 不要使用外部输入作为cache keys.
    • 使用expirations(过期时间)限制缓存增长
    • 使用SetSize, Size和SizeLimit来限制cache size. ASP.NET Core runtime不会根据memory pressure(内存压力)来限制cache size,它取决于开发者限制cache size.
Using IMemoryCache

In-memory caching 是一个从你的应用中使用依赖注入引入的服务(service)。在ConfigureServices中调用 AddMemoryCache:

代码语言:javascript复制
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMemoryCache();

        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseMvcWithDefaultRoute();
    }
}

Request the IMemoryCache实例在构造函数中:

代码语言:javascript复制
public class HomeController : Controller
{
    private IMemoryCache _cache;

    public HomeController(IMemoryCache memoryCache)
    {
        _cache = memoryCache;
    }

IMemoryCache要求有NuGet package Microsoft.Extensions.Caching.Memory, 它在Microsoft.AspNetCore.App metapackage也是可用的。

下面的代码使用 TryGetValue 来检验 if a time is in the cache. If a time isn’t cached, a new entry is created and added to the cache with Set. 检验一个时间值是否在缓存中。如果时间值没有被缓存,一个新的entry被创建并且with Set加入到缓存中。(即,如果没被缓存,则加入缓存)

代码语言:javascript复制
public static class CacheKeys
{
    public static string Entry { get { return "_Entry"; } }
    public static string CallbackEntry { get { return "_Callback"; } }
    public static string CallbackMessage { get { return "_CallbackMessage"; } }
    public static string Parent { get { return "_Parent"; } }
    public static string Child { get { return "_Child"; } }
    public static string DependentMessage { get { return "_DependentMessage"; } }
    public static string DependentCTS { get { return "_DependentCTS"; } }
    public static string Ticks { get { return "_Ticks"; } }
    public static string CancelMsg { get { return "_CancelMsg"; } }
    public static string CancelTokenSource { get { return "_CancelTokenSource"; } }
}
代码语言:javascript复制
public IActionResult CacheTryGetValueSet()
{
    DateTime cacheEntry;

    // Look for cache key.
    if (!_cache.TryGetValue(CacheKeys.Entry, out cacheEntry))
    {
        // Key not in cache, so get data.
        cacheEntry = DateTime.Now;

        // Set cache options.
        var cacheEntryOptions = new MemoryCacheEntryOptions()
            // Keep in cache for this time, reset time if accessed.
            .SetSlidingExpiration(TimeSpan.FromSeconds(3));

        // Save data in cache.
        _cache.Set(CacheKeys.Entry, cacheEntry, cacheEntryOptions);
    }

    return View("Cache", cacheEntry);
}

当前时间和缓存时间都被展示了:

代码语言:javascript复制
@model DateTime?

<div>
    <h2>Actions</h2>
    <ul>
        <li><a asp-controller="Home" asp-action="CacheTryGetValueSet">TryGetValue and Set</a></li>
        <li><a asp-controller="Home" asp-action="CacheGet">Get</a></li>
        <li><a asp-controller="Home" asp-action="CacheGetOrCreate">GetOrCreate</a></li>
        <li><a asp-controller="Home" asp-action="CacheGetOrCreateAsync">GetOrCreateAsync</a></li>
        <li><a asp-controller="Home" asp-action="CacheRemove">Remove</a></li>
    </ul>
</div>

<h3>Current Time: @DateTime.Now.TimeOfDay.ToString()</h3>
<h3>Cached Time: @(Model == null ? "No cached entry found" : Model.Value.TimeOfDay.ToString())</h3>

当requests在超时时间之内时,缓存的时间值保留在缓存中。下面的图片展示了当前时间和从缓存中检索的更早的时间。

下面的代码使用GetOrCreate和GetOrCreateAsync来缓存数据。

代码语言:javascript复制
public IActionResult CacheGetOrCreate()
{
    var cacheEntry = _cache.GetOrCreate(CacheKeys.Entry, entry =>
    {
        entry.SlidingExpiration = TimeSpan.FromSeconds(3);
        return DateTime.Now;
    });

    return View("Cache", cacheEntry);
}

public async Task<IActionResult> CacheGetOrCreateAsync()
{
    var cacheEntry = await
        _cache.GetOrCreateAsync(CacheKeys.Entry, entry =>
    {
        entry.SlidingExpiration = TimeSpan.FromSeconds(3);
        return Task.FromResult(DateTime.Now);
    });

    return View("Cache", cacheEntry);
}

下面的代码调用Get来取到缓存时间:

代码语言:javascript复制
public IActionResult CacheGet()
{
    var cacheEntry = _cache.Get<DateTime?>(CacheKeys.Entry);
    return View("Cache", cacheEntry);
}

GetOrCreate, GetOrCreateAsyc, 和Get 是CacheExtensions类的扩展方法的一部分,CacheExtension类扩展了IMemory的能力。

MemoryCacheEntryOptions

下面的例子(用来设置内存缓存的一些选项):

  • 设置完全的expiration time(超时时间)。这是这个记录可以被缓存的最大时间,并且可以防止这个记录变的太陈旧,当变化的expiration 是不断更新的。
  • 设置一个变化的expiration time. 到达这个缓存(cache item)的请求将重新设置变化的时间。
  • 给CacheItemPriority.NeverRemove设置cache priority(缓存优先级)
  • 设置PostEvictionDelegate(回调),它将会被调用,在记录被从缓存驱逐之后。Callback(回调)是运行在一个不同于从缓存中移除缓存项的线程的其他线程。(Callback运行在一个区别于移除缓存项线程的其他线程)
代码语言:javascript复制
public IActionResult CreateCallbackEntry()
{
    var cacheEntryOptions = new MemoryCacheEntryOptions()
        // Pin to cache.
        .SetPriority(CacheItemPriority.NeverRemove)
        // Add eviction callback
        .RegisterPostEvictionCallback(callback: EvictionCallback, state: this);

    _cache.Set(CacheKeys.CallbackEntry, DateTime.Now, cacheEntryOptions);

    return RedirectToAction("GetCallbackEntry");
}

public IActionResult GetCallbackEntry()
{
    return View("Callback", new CallbackViewModel
    {
        CachedTime = _cache.Get<DateTime?>(CacheKeys.CallbackEntry),
        Message = _cache.Get<string>(CacheKeys.CallbackMessage)
    });
}

public IActionResult RemoveCallbackEntry()
{
    _cache.Remove(CacheKeys.CallbackEntry);
    return RedirectToAction("GetCallbackEntry");
}

private static void EvictionCallback(object key, object value,
    EvictionReason reason, object state)
{
    var message = $"Entry was evicted. Reason: {reason}.";
    ((HomeController)state)._cache.Set(CacheKeys.CallbackMessage, message);
}
使用SetSize, Size和SizeLImit来限制 cache size

一个MemoryCache实例可以选择指定或者强制一个size limit。 The memory size limit 没有一个定义的测量单元,因为cache没有结构来测量记录(entries)大小(size). 如果cache memory size limit被设置了,所有的entries必须指定size. ASP.NET Core runtime不会根据memory pressure来limit cache size . 它取决于开发者limit cache size. The size spcified is in units the developer chooses.

A MemoryCache instance may optionally specify and enforce a size limit. The memory size limit does not have a defined unit of measure because the cache has no mechanism to measure the size of entries. If the cache memory size limit is set, all entries must specify size. The ASP.NET Core runtime does not limit cache size based on memory pressure. It's up to the developer to limit cache size. The size specified is in units the developer chooses.

例如:

  • 如果一个web应用主要caching string , 每个cache entry size应该是字符串长度
  • 应用可以指定the size of all entries (所有的entry)为1,并且这个size limit是the count of entries. (注意:这里指定所有的entry的大小为1,则size limit可以用entry的数量表示。即两者一个是cache entry size(单个entry大小),另一个是limit size(缓存限制的大小))

下面的代码创建了一个unitless fiexed size MemoryCache accessible(易接近的) by dependency injection:

代码语言:javascript复制
// using Microsoft.Extensions.Caching.Memory;
public class MyMemoryCache 
{
    public MemoryCache Cache { get; set; }
    public MyMemoryCache()
    {
        Cache = new MemoryCache(new MemoryCacheOptions
        {
            SizeLimit = 1024
        });
    }
}

SizeLimit没有 units . 如果cache memory size 已经被设置,Cached entries 必须指定最合适的size in whatever units they deem(认为)。一个cache 实例的所有用户应该用同样的unit system . 如果the sum of the cached entry sizes 超过通过SizeLimit指定的值, An entry 将不会被缓存. 如果no cache size limit被设置,the cache size set on the entry 将会被忽略。

SizeLimit does not have units. Cached entries must specify size in whatever units they deem most appropriate if the cache memory size has been set. All users of a cache instance should use the same unit system. An entry will not be cached if the sum of the cached entry sizes exceeds the value specified by SizeLimit. If no cache size limit is set, the cache size set on the entry will be ignored.

下面的代码使用依赖注入容器注册MyMemoryCache.

代码语言:javascript复制
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

    services.AddSingleton<MyMemoryCache>();
}

MyMemoryCache被创建为一个independent memory cache 的组件,这个组件了解size limited cahce并且知道怎么合适的设置cache entry size 。

下面是使用MyMemoryCache的代码:

代码语言:javascript复制
public class AboutModel : PageModel
{
    private MemoryCache _cache;
    public static readonly string MyKey = "_MyKey";

    public AboutModel(MyMemoryCache memoryCache)
    {
        _cache = memoryCache.Cache;
    }

    [TempData]
    public string DateTime_Now { get; set; }

    public IActionResult OnGet()
    {
        if (!_cache.TryGetValue(MyKey, out string cacheEntry))
        {
            // Key not in cache, so get data.
            cacheEntry = DateTime.Now.TimeOfDay.ToString();

            var cacheEntryOptions = new MemoryCacheEntryOptions() 
                // Set cache entry size by extension method.
                .SetSize(1) 
                // Keep in cache for this time, reset time if accessed.
                .SetSlidingExpiration(TimeSpan.FromSeconds(3));

            // Set cache entry size via property.
            // cacheEntryOptions.Size = 1;

            // Save data in cache.
            _cache.Set(MyKey, cacheEntry, cacheEntryOptions);
        }

        DateTime_Now = cacheEntry;

        return RedirectToPage("./Index");
    }
}

The size of the cache entry 可以被设置,通过Size和SetSize扩展方法

代码语言:javascript复制
public IActionResult OnGet()
{
    if (!_cache.TryGetValue(MyKey, out string cacheEntry))
    {
        // Key not in cache, so get data.
        cacheEntry = DateTime.Now.TimeOfDay.ToString();

        var cacheEntryOptions = new MemoryCacheEntryOptions() 
            // Set cache entry size by extension method.
            .SetSize(1) 
            // Keep in cache for this time, reset time if accessed.
            .SetSlidingExpiration(TimeSpan.FromSeconds(3));

        // Set cache entry size via property.
        // cacheEntryOptions.Size = 1;

        // Save data in cache.
        _cache.Set(MyKey, cacheEntry, cacheEntryOptions);
    }

    DateTime_Now = cacheEntry;

    return RedirectToPage("./Index");
}
Cache dependencies

下面的示例展示了怎么设置过期一个缓存记录(how to expire a cache entry)如果一个dependent entry expires(过期). 一个CancellationChangeToken 被加入到cached item. 当Cancel 在CancellationTokenSource上被调用,两个cache entry 都被抛弃.

代码语言:javascript复制
public IActionResult CreateDependentEntries()
{
    var cts = new CancellationTokenSource();
    _cache.Set(CacheKeys.DependentCTS, cts);

    using (var entry = _cache.CreateEntry(CacheKeys.Parent))
    {
        // expire this entry if the dependant entry expires.
        entry.Value = DateTime.Now;
        entry.RegisterPostEvictionCallback(DependentEvictionCallback, this);

        _cache.Set(CacheKeys.Child,
            DateTime.Now,
            new CancellationChangeToken(cts.Token));
    }

    return RedirectToAction("GetDependentEntries");
}

public IActionResult GetDependentEntries()
{
    return View("Dependent", new DependentViewModel
    {
        ParentCachedTime = _cache.Get<DateTime?>(CacheKeys.Parent),
        ChildCachedTime = _cache.Get<DateTime?>(CacheKeys.Child),
        Message = _cache.Get<string>(CacheKeys.DependentMessage)
    });
}

public IActionResult RemoveChildEntry()
{
    _cache.Get<CancellationTokenSource>(CacheKeys.DependentCTS).Cancel();
    return RedirectToAction("GetDependentEntries");
}

private static void DependentEvictionCallback(object key, object value,
    EvictionReason reason, object state)
{
    var message = $"Parent entry was evicted. Reason: {reason}.";
    ((HomeController)state)._cache.Set(CacheKeys.DependentMessage, message);
}

使用CancellationTokenSource允许多个cache entries作为一组被抛弃。使用代码中有用的模式,cache entires

Created inside the using block will inherit triggers and expiration settings.

Additional notes
  • 当用一个回调来增加一个cache item:
    • 可以发现多个requests缓存的键的value 是空.因为回调没有完成。
    • 这可能会导致几个线程增加cached item
  • 当一个cache entry被使用来创建另一个cache entry,child复制父母亲的entry的expration tokens 并且基于时间的exiration settings. THe child不会通过手动删除或者parent entry的更新而过期
  • 用PostEvictionCallback来设置callback, 这个callback将会被触发。

参考资料:

https://docs.microsoft.com/en-us/aspnet/core/performance/caching/memory?view=aspnetcore-2.2

0 人点赞