zoukankan      html  css  js  c++  java
  • C# .NET 中的缓存实现

    C# .NET 中的缓存实现

    软件开发中最常用的模式之一是缓存。这是一个简单但非常有效的概念,这个想法的核心是记录过程数据,重用操作结果。当执行繁重的操作时,我们会将结果保存在我们的缓存容器中。下次我们需要该结果时,我们将从缓存容器中拉出它,而不是再次执行繁重的操作。

    例如,要获取一个人的头像,您可能需要访问数据库。我们不会每次都执行那次旅行,而是将 Avatar 保存在缓存中,每次需要时从内存中提取它。

    缓存非常适用于不经常更改的数据。或者甚至更好,永远不会改变。不断变化的数据,比如当前机器的时间不应该被缓存,否则你会得到错误的结果。

    进程内缓存、持久性进程内缓存和分布式缓存

    有 3 种类型的缓存:

    In-Memory Cache用于在单个进程中实现缓存。当进程终止时,缓存也随之终止。如果您在多台服务器上运行相同的进程,您将为每台服务器提供一个单独的缓存。•持久性进程内缓存是指在进程内存之外备份缓存。它可能在文件中,也可能在数据库中。这比较困难,但如果您的进程重新启动,缓存不会丢失。最适合在获取缓存项的情况下使用范围广泛,并且您的进程往往会重新启动很多。•分布式缓存是指您希望为多台机器共享缓存。通常,它将是多个服务器。使用分布式缓存,它存储在外部服务中。这意味着如果一台服务器保存了一个缓存项,其他服务器也可以使用它。像Redis[1]这样的服务非常适合这一点。

    我们将只讨论进程内缓存

    早期做法

    让我们用 C# 创建一个非常简单的缓存实现:

    public class NaiveCache<TItem>
    {
        Dictionary<object, TItem> _cache = new Dictionary<object, TItem>();
    
        public TItem GetOrCreate(object key, Func<TItem> createItem)
        {
            if (!_cache.ContainsKey(key))
            {
                _cache[key] = createItem();
            }
            return _cache[key];
        }
    }

    用法:

    var _avatarCache = new NaiveCache<byte[]>();// ...
    var myAvatar = _avatarCache.GetOrCreate(userId, () => _database.GetAvatar(userId));

    这个简单的代码解决了一个关键问题。要获取用户的头像,只有第一个请求才会真正执行到数据库的访问。然后将头像数据 ( byte[]) 保存在进程内存中。对头像的所有后续请求都将从内存中提取,从而节省时间和资源。

    但是,正如编程中的大多数事情一样,没有什么是那么简单的。由于多种原因,上述解决方案并不好。一方面,这个实现不是线程安全的。从多个线程使用时可能会发生异常。除此之外,缓存的项目将永远留在内存中,这实际上非常糟糕。

    这就是我们应该从缓存中删除项目的原因:

    1.缓存会占用大量内存,最终导致内存不足异常和崩溃。2.高内存消耗会导致GC 压力(又名内存压力)。在这种状态下,垃圾收集器的工作量超出其应有的水平,从而损害了性能。3.如果数据发生变化,可能需要刷新缓存。我们的缓存基础设施应该支持这种能力。

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

    •无论如何,绝对过期策略将在固定时间后从缓存中删除项目。•如果在固定的时间段内未访问某个项目,则滑动过期策略将从缓存中删除该项目。因此,如果我将过期时间设置为 1 分钟,只要我每 30 秒使用一次,该项目就会一直保留在缓存中。一旦我超过一分钟不使用它,该物品就会被驱逐。•大小限制策略将限制缓存内存大小。

    现在我们知道我们需要什么,让我们继续寻找更好的解决方案。

    更好的解决方案

    作为一名博主,令我非常沮丧的是,微软已经创建了一个很棒的缓存实现。这剥夺了我自己创建类似实现的乐趣,但至少我写这篇博文的工作量减少了。

    我将向您展示微软的解决方案,如何有效地使用它,然后在某些场景中如何改进它。

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

    Microsoft 有 2 个解决方案 2 个不同的 NuGet 包用于缓存。两者都很棒。根据 Microsoft 的建议[2],更喜欢使用,Microsoft.Extensions.Caching.Memory因为它与 Asp.NET Core 集成得更好。它可以很容易地注入[3]到 Asp .NET Core 的依赖注入机制中。

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

    public class SimpleMemoryCache<TItem>
    {
        private MemoryCache _cache = new MemoryCache(new MemoryCacheOptions());
    
        public TItem GetOrCreate(object key, Func<TItem> createItem)
        {
            TItem cacheEntry;
            if (!_cache.TryGetValue(key, out cacheEntry))// Look for cache key.
            {
                // Key not in cache, so get data.
                cacheEntry = createItem();
    
                // Save data in cache.
                _cache.Set(key, cacheEntry);
            }
            return cacheEntry;
        }
    }

    用法:

    var _avatarCache = new SimpleMemoryCache<byte[]>();// ...
    var myAvatar = _avatarCache.GetOrCreate(userId, () => _database.GetAvatar(userId));

    这和我自己的非常相似NaiveCache,所以有什么改变?嗯,一方面,这是一个线程安全的实现。您可以一次从多个线程安全地调用它。

    第二件事是MemoryCache允许我们之前谈到的所有驱逐政策。下面是一个例子:

    具有驱逐策略的 IMemoryCache:

    public class MemoryCacheWithPolicy<TItem>
    {
        private MemoryCache _cache = new MemoryCache(new MemoryCacheOptions()
        {
            SizeLimit = 1024
        });
    
        public TItem GetOrCreate(object key, Func<TItem> createItem)
        {
            TItem cacheEntry;
            if (!_cache.TryGetValue(key, out cacheEntry))// Look for cache key.
            {
                // Key not in cache, so get data.
                cacheEntry = createItem();
    
                var cacheEntryOptions = new MemoryCacheEntryOptions()
                 .SetSize(1)//Size amount
                 //Priority on removing when reaching size limit (memory pressure)
                    .SetPriority(CacheItemPriority.High)
                    // Keep in cache for this time, reset time if accessed.
                    .SetSlidingExpiration(TimeSpan.FromSeconds(2))
                    // Remove from cache after this time, regardless of sliding expiration
                    .SetAbsoluteExpiration(TimeSpan.FromSeconds(10));
    
                // Save data in cache.
                _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 秒请求头像,它将检查头像是否已缓存(尚未缓存),并开始另一次访问数据库。

    关于GC压力的第一个问题:可以使用多种技术和启发式方法来监控GC压力。这篇博文与此无关,但您可以阅读我的文章在 C# .NET 中查找、修复和避免内存泄漏:8 个最佳实践[4]以了解一些有用的方法。

    第二个问题更容易解决。事实上,这是一个MemoryCache完全解决它的实现:

    public class WaitToFinishMemoryCache<TItem>
    {
        private MemoryCache _cache = new MemoryCache(new MemoryCacheOptions());
        private ConcurrentDictionary<object, SemaphoreSlim> _locks = new ConcurrentDictionary<object, SemaphoreSlim>();
    
        public async Task<TItem> GetOrCreate(object key, Func<Task<TItem>> createItem)
        {
            TItem cacheEntry;
    
            if (!_cache.TryGetValue(key, out cacheEntry))// Look for cache key.
            {
                SemaphoreSlim mylock = _locks.GetOrAdd(key, k => new SemaphoreSlim(1, 1));
    
                await mylock.WaitAsync();
                try
                {
                    if (!_cache.TryGetValue(key, out cacheEntry))
                    {
                        // Key not in cache, so get data.
                        cacheEntry = await createItem();
                        _cache.Set(key, cacheEntry);
                    }
                }
                finally
                {
                    mylock.Release();
                }
            }
            return cacheEntry;
        }
    }
     

    用法:

    var _avatarCache = new WaitToFinishMemoryCache<byte[]>();// ...
    var myAvatar =  await _avatarCache.GetOrCreate(userId, async () => await _database.GetAvatar(userId));

    代码说明

    此实现锁定项目的创建。锁是特定于钥匙的。例如,如果我们正在等待获取 Alex 的 Avatar,我们仍然可以在另一个线程上获取 John 或 Sarah 的缓存值。

    字典_locks存储了所有的锁。常规锁不适用于async/await,因此我们需要使用SemaphoreSlim[5].

    如果 (!_cache.TryGetValue(key, out cacheEntry)),有 2 次检查以查看该值是否已被缓存。锁内的那个是确保只有一个创建的那个。锁外面的那个是为了优化。

    何时使用 WaitToFinishMemoryCache

    这个实现显然有一些开销。让我们考虑什么时候甚至有必要。

    在以下情况下使用 WaitToFinishMemoryCache:

    •当项目的创建时间具有某种成本时,您希望尽可能减少创建。•当一个项目的创建时间很长时。•当必须确保每个键都创建一个项目时。

    在以下情况下不要使用 WaitToFinishMemoryCache:

    •没有多个线程访问同一个缓存项的危险。•您不介意多次创建该项目。例如,如果对数据库的额外访问不会有太大变化。

    概括

    缓存是一种非常强大的模式,它也很危险,并且有其自身的复杂性。缓存太多,可能会导致 GC 压力,缓存太少会导致性能问题。而分布式缓存,这是一个需要探索的全新世界。软件开发职业就这样,总是有新的东西要学习。

  • 相关阅读:
    CSS盒子模型
    getContextPath、getServletPath、getRequestURI、request.getRealPath的区别
    MYSQL中的CASE WHEN END AS
    单点登录的精华总结
    git&github
    June 21st 2017 Week 25th Wednesday
    June 20th 2017 Week 25th Tuesday
    June 19th 2017 Week 25th Monday
    June 18th 2017 Week 25th Sunday
    June 17th 2017 Week 24th Saturday
  • 原文地址:https://www.cnblogs.com/dongh/p/15457097.html
Copyright © 2011-2022 走看看