.NET中的缓存实现

软件开发中最经常使用的模式之一是缓存,这是一个简单但很是有效的概念,想法是重用操做结果,执行繁重的操做时,咱们会将结果保存在缓存容器中,下次咱们须要该结果时,咱们将从缓存容器中取出它,而不是再次执行繁重的操做。web

例如,要得到某人的头像,您可能须要前往数据库。咱们不会每次都执行那次查询,而是将结果保存在缓存中,每次须要时都将其从内存中删除。redis

缓存很是适合不常常更改的数据,甚至永远不会改变。不断变化的数据不适合缓存,如当前机器的时间不该缓存,不然您将获得错误的结果。数据库

进程内缓存,持久化缓存和分布式缓存

  • 进程内缓存用于在单个进程中实现缓存时,当进程终止时,缓存会随之消失。若是您在多个服务器上运行相同的进程,则每一个服务器都有一个单独的缓存。
  • 持久化缓存是指在进程内存以外备份缓存,它可能位于文件中,也可能位于数据库中。这实现比较困难,但若是从新启动进程,缓存不会丢失。
  • 分布式缓存是指您为多台计算机提供共享缓存,一般它将是几个服务器,使用分布式缓存,它存储在外部服务中。这意味着若是一台服务器保存了缓存项,其余服务器也可使用它。Redis这样的服务很是适合这种状况。

单线程的缓存

public class NaiveCache<T>
{
    private static Dictionary<object, T> _cache = new Dictionary<object, T>();
    public static T GetOrCreate(object key, Func<T> createItem)
    {
        T cacheEntry;
        if (!_cache.TryGetValue(key, out cacheEntry))
        {
            cacheEntry = createItem();
            _cache.Add(key, cacheEntry);
        }

        return cacheEntry;
    }
}

//用法
NaiveCache<string>.GetOrCreate("test", () => { return "test123"; });

这个简单的代码解决了一个关键问题,要获取test的值,只有第一个请求才会实际执行数据库操做,而后将数据保存在进程存储器中,之后有关test的请求都将从内存中提取,从而节省时间和资源。编程

可是,做为编程中的大多数事情,没有什么是如此简单。因为许多缘由,上述解决方案并很差。首先,这种实现不是线程安全的,多个线程使用时可能会发生异常,除此以外,缓存的项目将永远留在内存中,这实际上很是糟糕。缓存

例如:安全

List<Task> t1 = new List<Task>();

foreach (var item in list)
{
    var a = Task.Run(() =>
    {
        Console.Write($"{NaiveCache<string>.GetOrCreate(item, () => { return item.ToString(); })}");
    });
    t1.Add(a);
}

try
{
    Task.WaitAll(t1.ToArray());
}
catch { }

运行结果7234859,运行 的数据丢失了服务器

这就是为何咱们应该从Cache中删除项目:

  1. 缓存可能占用大量内存,最终致使内存不足异常和崩溃。
  2. 高内存消耗可致使GC压力(又称内存压力)。在这种状态下,垃圾收集器的工做量超出预期,会影响性能。
  3. 若是数据发生更改,可能须要刷新缓存,咱们的缓存基础架构应该支持这种能力。

为了处理这些问题,缓存框架具备驱逐策略(即删除策略),这些是根据某些逻辑从缓存中删除项目的规则,常见的驱逐政策是:架构

  • 绝对过时策略将在一段固定的时间后从缓存中删除一个项目。
  • 若是未在固定的时间内访问项目,则滑动过时策略将从缓存中删除项目所以,若是我将到期时间设置为1分钟,只要我每隔30秒使用一次,该项目就会保持在缓存中,一旦我不使用它超过一分钟,该项目被驱逐。
  • 大小限制策略将限制高速缓存大小。

如今咱们知道了咱们须要什么,让咱们继续寻找更好的解决方案。框架

改善方案

令我很是沮丧的是,做为博主,微软已经建立了一个很棒的缓存实现,这剥夺了我本身建立相似实现的乐趣,但至少我写这篇博文的工做较少。分布式

我将向您展现Microsoft的解决方案,如何有效地使用它,以及如何在某些状况下改进它。

System.Runtime.Caching / MemoryCache与Microsoft.Extensions.Caching.Memory

微软有2个解决方案,2个不一样的NuGet包用于缓存,二者都很棒,根据微软的建议,更喜欢使用Microsoft.Extensions.Caching.Memory由于它与Asp更好地集成.NET核心。它能够很容易地注入到Asp .NET Core的依赖注入机制中。

这是一个基本的例子Microsoft.Extensions.Caching.Memory

/// <summary>
/// 利用微软的库写的缓存
/// </summary>
/// <typeparam name="T"></typeparam>
public class SimpleMemoyCache<T>
{
    private static MemoryCache _cache = new MemoryCache(new MemoryCacheOptions());

    public static T GetOrCreate(object key, Func<T> createItem) {
        T cacheEntry;
        if (!_cache.TryGetValue(key, out cacheEntry)) {
            cacheEntry = createItem();
            _cache.Set(key, cacheEntry);
        }

        return cacheEntry;
    }
}

用法:

SimpleMemoyCache<string>.GetOrCreate("test", () => { return "test123"; });

这与我本身很是类似NaiveCache,因此改变了什么?嗯,首先,这是一个线程安全的实现。您能够安全地从多个线程一次调用它。

带有逐出政策的IMemoryCache:

/// <summary>
/// 带有策略的缓存
/// </summary>
/// <typeparam name="T"></typeparam>
public class MemoryCacheWithPolicy<T>
{
    /// <summary>
    /// 增长设置缓存大小
    /// </summary>
    private static MemoryCache _cache = new MemoryCache(new MemoryCacheOptions() { SizeLimit = 1024 });

    public static T GetOrCreate(object key, Func<T> createItem) {
        T cacheEntry;
        if (!_cache.TryGetValue(key, out cacheEntry)) {
            cacheEntry = createItem();
            var cacheEntryOptions = new MemoryCacheEntryOptions()
                .SetSize(1)
                .SetPriority(CacheItemPriority.High) //设置优先级
                .SetSlidingExpiration(TimeSpan.FromSeconds(2)) //2s没有访问删除
                .SetAbsoluteExpiration(TimeSpan.FromSeconds(10)); //10s过时

            _cache.Set(key, cacheEntry, cacheEntryOptions);
        }

        return cacheEntry;
    }
}

让咱们分析一下新增内容:

  1. SizeLimit加入了MemoryCacheOptions,这会将基于大小的策略添加到缓存容器中。相反,咱们须要在每一个缓存条目上设置大小,在这种状况下,咱们每次设置为1 SetSize(1),这意味着缓存限制为1024个项目。
  2. 当咱们达到大小限制时,应该删除哪一个缓存项?您实际上能够设置优先级.SetPriority(CacheItemPriority.High)级别为Low,Normal,HighNeverRemove
  3. SetSlidingExpiration(TimeSpan.FromSeconds(2))添加了,将滑动到期时间设置为2秒,这意味着若是超过2秒内未访问某个项目,它将被删除。
  4. SetAbsoluteExpiration(TimeSpan.FromSeconds(10))添加了,它将绝对到期时间设置为10秒,这意味着若是物品还没有在10秒内被驱逐。

除了示例中的选项以外,您还能够设置一个RegisterPostEvictionCallback委托,当项目被驱逐时将调用委托。

这是一个很是全面的功能集。它让你想知道是否还有其余东西要添加,实际上有几件事。

问题和缺失的功能

这个实现中有几个重要的缺失部分。

  1. 虽然您能够设置大小限制,但缓存实际上并不监视gc压力。若是咱们确实对其进行监控,咱们能够在压力较大时收紧政策,并在压力较低时放松政策。
  2. 当同时请求具备多个线程的相同项时,请求不等待第一个完成,该项目将被屡次建立。例如,假设咱们正在缓存阿凡达,从数据库中获取头像须要10秒钟,若是咱们在第一次请求后2秒请求头像,它将检查头像是否被缓存(它尚未),并开始另外一次访问数据库。

英文原文中有说明,可是以为不太好,再次没有翻译。

英文原文地址:

https://michaelscodingspot.com/cache-implementations-in-csharp-net/?utm_source=csharpdigest&utm_medium=web&utm_campaign=featured

代码与所写有所修改,可是大体意思同样,若是感兴趣,能够看看英文。

相关文章
相关标签/搜索