C#/.Net 中的缓存技术

39
我想问一下,在C#中实现缓存的最佳方法是什么?是否有可能使用.NET类或类似的东西来实现?也许可以使用像字典这样的东西,如果它变得太大,它可以删除一些条目,但这些条目不会被垃圾回收器删除?

2
它非常依赖于应用程序。你要用它做什么? - Sam Harwell
不是以ASP.NET的方式,但我还不确定,等我得到要求后再发布它们,但感谢您的第一个答案 :) - Sebastian Müller
《强大的.NET缓存》(http://www.codeducky.org/robust-net-caching/)涵盖了缓存的常见陷阱,并提供了一个库,帮助开发人员避免一些常见的问题。文章特别解释了如何安全地使用[MemoryCache](http://msdn.microsoft.com/en-us/library/system.runtime.caching.memorycache(v=vs.110).aspx)。 - Steven Wexler
1
这篇文章值得一读:http://www.jondavis.net/techblog/post/2010/08/30/Four-Methods-Of-Simple-Caching-In-NET.aspx - AFract
12个回答

29

如果您正在使用.NET 4或更高版本,您可以使用MemoryCache类。


20

如果您正在使用ASP.NET,可以使用Cache类(System.Web.Caching)。

这里有一个很好的辅助类:c-cache-helper-class

如果您指的是Windows窗体应用程序中的缓存,那取决于您尝试做什么以及在何处缓存数据。

我们已经针对某些方法实现了Web服务后面的缓存(使用System.Web.Caching对象)。

但是,您可能还需要查看Caching Application Block。(请参见此处),它是.NET Framework 2.0的企业库的一部分。


2
我建议了一个针对ASP.NET的选项,以及在没有ASP.NET时的一种方法(缓存应用程序块)。 - Bravax
不管是不是asp.net,你都可以在桌面应用程序中引用System.Web,并通过HttpRuntime.Cache属性使用System.Web.Cache。 - Ricardo Nolde
@RicardoNolde 但这取决于他链接的是哪个 .net 框架。如果他正在使用 Windows 应用程序的默认框架 (客户端框架),则 System.Web 命名空间不可用。 - m__
MemoryCache在较新版本的.NET中可用于那些不使用ASP.NET的开发者。 - jpaugh
链接已失效,这里是缓存版本 - Matt R

5

MemoryCache是框架中一个不错的起点,但您也可以考虑使用开源库LazyCache,因为它具有比内存缓存更简单的API,并且具有内置锁定以及其他一些对开发人员友好的功能。它也可以在nuget上获得。

这里给您举个例子:

// Create our cache service using the defaults (Dependency injection ready).
// Uses MemoryCache.Default as default so cache is shared between instances
IAppCache cache = new CachingService();

// Declare (but don't execute) a func/delegate whose result we want to cache
Func<ComplexObjects> complexObjectFactory = () => methodThatTakesTimeOrResources();

// Get our ComplexObjects from the cache, or build them in the factory func 
// and cache the results for next time under the given key
ComplexObject cachedResults = cache.GetOrAdd("uniqueKey", complexObjectFactory);

我最近写了一篇关于在.NET应用程序中使用缓存的文章,您可能会发现它很有用。请参考此链接
(免责声明:我是LazyCache的作者)

比MemoryCache更简单的API?我发现这很难相信,因为这可以说是微软提供的最简单的API之一。我也在努力弄清楚你为什么要构建一个全新的缓存API的确切原因?它看起来很不错,但为什么要为那些已经存在且稳定了很长时间的东西付出大量心血呢? - hbulens
2
这并不是对内存缓存的重大更改,但它处理了并发问题,并通过一行可缓存委托减少了重复。由于我多次编写相同的粗略代码,所以拥有一个库可以节省我的时间,这已经足够好了。 - alastairtree

3
.NET提供的缓存类很方便,但存在一个主要问题 - 如果长时间存储数千万个以上的对象,则无法存储太多数据而不会影响GC。如果您仅缓存少量对象,则它们功能良好。但是,一旦您进入百万级别并将它们保留到GEN2,当系统达到低内存阈值且GC需要扫描所有代时,GC暂停最终会开始变得明显。
实际上,如果您需要存储几十万个实例,请使用MS缓存。不管您的对象是2个还是25个字段 - 它与引用数量有关。
另一方面,这里存在一种情况,即需要利用大型RAM(现在很常见),例如64 GB。 为此,我们创建了一个100%托管的内存管理器和缓存,其位于其之上。
我们的解决方案可以轻松地在进程内存中存储300,000,000个对象而完全不会影响GC - 这是因为我们将数据存储在大的(250 MB)字节数组段中。
以下是代码:NFX Pile (Apache 2.0)
视频链接如下: NFX Pile Cache - Youtube


2

2
如其他答案中所提到的,使用.NET Framework的默认选择是MemoryCache以及各种相关的Microsoft NuGet包(例如Microsoft.Extensions.Caching.MemoryCache)。所有这些缓存都是以使用内存为尺寸限制,并尝试通过跟踪总物理内存相对于缓存对象数量的增加来估算内存使用情况。然后一个后台线程会定期“修剪”条目。

MemoryCache等存在一些限制:

  • 键是字符串,因此如果键类型不是本地字符串,则必须不断在堆上分配字符串。当项目处于“热”状态时,在服务器应用程序中可能会增加很多。
  • 具有较差的“扫描阻力” - 例如,如果某个自动化过程正在快速循环遍历所有现有项,则缓存大小可能会增长得太快,以至于后台线程无法跟上。这可能会导致内存压力、页面故障、诱发GC或在运行IIS时,由于超过私有字节限制而回收进程。
  • 不适合并发写入。
  • 包含无法禁用的性能计数器(会产生开销)。

您的工作负载将决定这些问题的严重程度。缓存的替代方法是限制缓存中对象的数量(而不是估算使用的内存)。然后,缓存替换策略确定在缓存已满时要丢弃哪个对象。

下面是一个简单缓存的源代码,采用最近最少使用的驱逐策略:

public sealed class ClassicLru<K, V>
{
    private readonly int capacity;
    private readonly ConcurrentDictionary<K, LinkedListNode<LruItem>> dictionary;
    private readonly LinkedList<LruItem> linkedList = new LinkedList<LruItem>();

    private long requestHitCount;
    private long requestTotalCount;

    public ClassicLru(int capacity)
        : this(Defaults.ConcurrencyLevel, capacity, EqualityComparer<K>.Default)
    { 
    }

    public ClassicLru(int concurrencyLevel, int capacity, IEqualityComparer<K> comparer)
    {
        if (capacity < 3)
        {
            throw new ArgumentOutOfRangeException("Capacity must be greater than or equal to 3.");
        }

        if (comparer == null)
        {
            throw new ArgumentNullException(nameof(comparer));
        }

        this.capacity = capacity;
        this.dictionary = new ConcurrentDictionary<K, LinkedListNode<LruItem>>(concurrencyLevel, this.capacity + 1, comparer);
    }

    public int Count => this.linkedList.Count;

    public double HitRatio => (double)requestHitCount / (double)requestTotalCount;

    ///<inheritdoc/>
    public bool TryGet(K key, out V value)
    {
        Interlocked.Increment(ref requestTotalCount);

        LinkedListNode<LruItem> node;
        if (dictionary.TryGetValue(key, out node))
        {
            LockAndMoveToEnd(node);
            Interlocked.Increment(ref requestHitCount);
            value = node.Value.Value;
            return true;
        }

        value = default(V);
        return false;
    }

    public V GetOrAdd(K key, Func<K, V> valueFactory)
    {
        if (this.TryGet(key, out var value))
        {
            return value;
        }

        var node = new LinkedListNode<LruItem>(new LruItem(key, valueFactory(key)));

        if (this.dictionary.TryAdd(key, node))
        {
            LinkedListNode<LruItem> first = null;

            lock (this.linkedList)
            {
                if (linkedList.Count >= capacity)
                {
                    first = linkedList.First;
                    linkedList.RemoveFirst();
                }

                linkedList.AddLast(node);
            }

            // Remove from the dictionary outside the lock. This means that the dictionary at this moment
            // contains an item that is not in the linked list. If another thread fetches this item, 
            // LockAndMoveToEnd will ignore it, since it is detached. This means we potentially 'lose' an 
            // item just as it was about to move to the back of the LRU list and be preserved. The next request
            // for the same key will be a miss. Dictionary and list are eventually consistent.
            // However, all operations inside the lock are extremely fast, so contention is minimized.
            if (first != null)
            {
                dictionary.TryRemove(first.Value.Key, out var removed);

                if (removed.Value.Value is IDisposable d)
                {
                    d.Dispose();
                }
            }

            return node.Value.Value;
        }

        return this.GetOrAdd(key, valueFactory);
    }

    public bool TryRemove(K key)
    {
        if (dictionary.TryRemove(key, out var node))
        {
            // If the node has already been removed from the list, ignore.
            // E.g. thread A reads x from the dictionary. Thread B adds a new item, removes x from 
            // the List & Dictionary. Now thread A will try to move x to the end of the list.
            if (node.List != null)
            {
                lock (this.linkedList)
                {
                    if (node.List != null)
                    {
                        linkedList.Remove(node);
                    }
                }
            }

            if (node.Value.Value is IDisposable d)
            {
                d.Dispose();
            }

            return true;
        }

        return false;
    }

    // Thead A reads x from the dictionary. Thread B adds a new item. Thread A moves x to the end. Thread B now removes the new first Node (removal is atomic on both data structures).
    private void LockAndMoveToEnd(LinkedListNode<LruItem> node)
    {
        // If the node has already been removed from the list, ignore.
        // E.g. thread A reads x from the dictionary. Thread B adds a new item, removes x from 
        // the List & Dictionary. Now thread A will try to move x to the end of the list.
        if (node.List == null)
        {
            return;
        }

        lock (this.linkedList)
        {
            if (node.List == null)
            {
                return;
            }

            linkedList.Remove(node);
            linkedList.AddLast(node);
        }
    }

    private class LruItem
    {
        public LruItem(K k, V v)
        {
            Key = k;
            Value = v;
        }

        public K Key { get; }

        public V Value { get; }
    }
}

这只是为了说明一个线程安全的缓存 - 它可能存在错误,并且在重负载(例如在Web服务器中)下可能成为瓶颈。
经过充分测试、可生产的、可扩展的并发实现超出了 Stack Overflow 帖子的范围。为了解决这个问题,我在我的项目中实现了一个线程安全的伪LRU(类似于并发字典,但有限制大小)。性能非常接近原始的ConcurrentDictionary,比MemoryCache快约10倍,比上面的ClassicLru具有更好的并发吞吐量和更高的命中率。在下面的github链接中提供了详细的性能分析。
使用方法如下:
int capacity = 666;
var lru = new ConcurrentLru<int, SomeItem>(capacity);

var value = lru.GetOrAdd(1, (k) => new SomeItem(k));

GitHub: https://github.com/bitfaster/BitFaster.Caching
Install-Package BitFaster.Caching

0

您的问题需要更多的澄清。C#是一种语言而不是框架。您必须指定要实现缓存的框架。如果我们考虑您想在ASP.NET中实现它,那么它完全取决于您从缓存中想要什么。您可以在进程内缓存之间进行选择(这将使数据保留在应用程序的堆内),或者在进程外缓存之间进行选择(在这种情况下,您可以将数据存储在除堆之外的其他内存中,例如Amazon Elastic缓存服务器)。还有另一个决策需要做出,即客户端缓存或服务器端缓存之间的选择。通常,在解决方案中,您必须为缓存不同的数据开发不同的解决方案。因为基于四个因素(可访问性、持久性、大小、成本),您必须决定需要哪种解决方案。


0

我之前写过这个程序,看起来运行得很好。它允许您通过使用不同的类型来区分不同的缓存存储:ApplicationCaching<MyCacheType1>, ApplicationCaching<MyCacheType2>...

您可以决定允许某些存储在执行后持久化,而其他存储则会过期。

您需要引用Newtonsoft.Json序列化器(或使用替代品),当然所有要缓存的对象或值类型都必须可序列化。

使用MaxItemCount来设置任何一个存储中项目数量的限制。

一个单独的Zipper类(请参见下面的代码)使用System.IO.Compression。这最小化了存储的大小,并有助于加快加载时间。


public static class ApplicationCaching<K> 
{
        //====================================================================================================================
        public static event EventHandler InitialAccess = (s, e) => { };
        //=============================================================================================
        static Dictionary<string, byte[]> _StoredValues;
        static Dictionary<string, DateTime> _ExpirationTimes = new Dictionary<string, DateTime>();
        //=============================================================================================
        public static int MaxItemCount { get; set; } = 0;
        private static void OnInitialAccess()
        {
            //-----------------------------------------------------------------------------------------
            _StoredValues = new Dictionary<string, byte[]>();
            //-----------------------------------------------------------------------------------------
            InitialAccess?.Invoke(null, EventArgs.Empty);
            //-----------------------------------------------------------------------------------------
        }
        public static void AddToCache<T>(string key, T value, DateTime expirationTime)
        {
            try
            {
                //-----------------------------------------------------------------------------------------
                if (_StoredValues is null) OnInitialAccess();
                //-----------------------------------------------------------------------------------------
                string strValue = JsonConvert.SerializeObject(value);
                byte[] zippedValue = Zipper.Zip(strValue);
                //-----------------------------------------------------------------------------------------
                _StoredValues.Remove(key);
                _StoredValues.Add(key, zippedValue);
                //-----------------------------------------------------------------------------------------
                _ExpirationTimes.Remove(key);
                _ExpirationTimes.Add(key, expirationTime);
                //-----------------------------------------------------------------------------------------
            }
            catch (Exception ex)
            {

                throw ex;
            }
        }
        //=============================================================================================
        public static T GetFromCache<T>(string key, T defaultValue = default)
        {
            try
            {
                //-----------------------------------------------------------------------------------------
                if (_StoredValues is null) OnInitialAccess();
                //-----------------------------------------------------------------------------------------
                if (_StoredValues.ContainsKey(key))
                {
                    //------------------------------------------------------------------------------------------
                    if (_ExpirationTimes[key] <= DateTime.Now)
                    {
                        //------------------------------------------------------------------------------------------
                        _StoredValues.Remove(key);
                        _ExpirationTimes.Remove(key);
                        //------------------------------------------------------------------------------------------
                        return defaultValue;
                        //------------------------------------------------------------------------------------------
                    }
                    //------------------------------------------------------------------------------------------
                    byte[] zippedValue = _StoredValues[key];
                    //------------------------------------------------------------------------------------------
                    string strValue = Zipper.Unzip(zippedValue);
                    T value = JsonConvert.DeserializeObject<T>(strValue);
                    //------------------------------------------------------------------------------------------
                    return value;
                    //------------------------------------------------------------------------------------------
                }
                else
                {
                    return defaultValue;
                }
                //---------------------------------------------------------------------------------------------
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }
        //=============================================================================================
        public static string ConvertCacheToString()
        {
            //-----------------------------------------------------------------------------------------
            if (_StoredValues is null || _ExpirationTimes is null) return "";
            //-----------------------------------------------------------------------------------------
            List<string> storage = new List<string>();
            //-----------------------------------------------------------------------------------------
            string strStoredObject = JsonConvert.SerializeObject(_StoredValues);
            string strExpirationTimes = JsonConvert.SerializeObject(_ExpirationTimes);
            //-----------------------------------------------------------------------------------------
            storage.AddRange(new string[] { strStoredObject, strExpirationTimes});
            //-----------------------------------------------------------------------------------------
            string strStorage = JsonConvert.SerializeObject(storage);
            //-----------------------------------------------------------------------------------------
            return strStorage;
            //-----------------------------------------------------------------------------------------
        }
        //=============================================================================================
        public static void InializeCacheFromString(string strCache)
        {
            try
            {
                //-----------------------------------------------------------------------------------------
                List<string> storage = JsonConvert.DeserializeObject<List<string>>(strCache);
                //-----------------------------------------------------------------------------------------
                if (storage != null && storage.Count == 2)
                {
                    //-----------------------------------------------------------------------------------------
                    _StoredValues = JsonConvert.DeserializeObject<Dictionary<string, byte[]>>(storage.First());
                    _ExpirationTimes = JsonConvert.DeserializeObject<Dictionary<string, DateTime>>(storage.Last());
                    //-----------------------------------------------------------------------------------------
                    if (_ExpirationTimes != null && _StoredValues != null)
                    {
                        //-----------------------------------------------------------------------------------------
                        for (int i = 0; i < _ExpirationTimes.Count; i++)
                        {
                            string key = _ExpirationTimes.ElementAt(i).Key;
                            //-----------------------------------------------------------------------------------------
                            if (_ExpirationTimes[key] < DateTime.Now)
                            {
                                ClearItem(key);
                            }
                            //-----------------------------------------------------------------------------------------
                        }

                        //-----------------------------------------------------------------------------------------
                        if (MaxItemCount > 0 && _StoredValues.Count > MaxItemCount)
                        {
                            IEnumerable<KeyValuePair<string, DateTime>> countedOutItems = _ExpirationTimes.OrderByDescending(o => o.Value).Skip(MaxItemCount);
                            for (int i = 0; i < countedOutItems.Count(); i++)
                            {
                                ClearItem(countedOutItems.ElementAt(i).Key);
                            }
                        }
                        //-----------------------------------------------------------------------------------------
                        return;
                        //-----------------------------------------------------------------------------------------
                    }
                    //-----------------------------------------------------------------------------------------
                }
                //-----------------------------------------------------------------------------------------
                _StoredValues = new Dictionary<string, byte[]>();
                _ExpirationTimes = new Dictionary<string, DateTime>();
                //-----------------------------------------------------------------------------------------
            }
            catch (Exception)
            {
                throw;
            }
        }
        //=============================================================================================
        public static void ClearItem(string key)
        {
            //-----------------------------------------------------------------------------------------
            if (_StoredValues.ContainsKey(key))
            {
                _StoredValues.Remove(key);
            }
            //-----------------------------------------------------------------------------------------
            if (_ExpirationTimes.ContainsKey(key))
                _ExpirationTimes.Remove(key);
            //-----------------------------------------------------------------------------------------
        }
        //=============================================================================================
    }

您可以轻松地使用类似以下代码的缓存功能...

            //------------------------------------------------------------------------------------------------------------------------------
            string key = "MyUniqueKeyForThisItem";
            //------------------------------------------------------------------------------------------------------------------------------
            MyType obj = ApplicationCaching<MyCacheType>.GetFromCache<MyType>(key);
            //------------------------------------------------------------------------------------------------------------------------------

            if (obj == default)
            {
                obj = new MyType(...);
                ApplicationCaching<MyCacheType>.AddToCache(key, obj, DateTime.Now.AddHours(1));
            }


请注意,缓存中存储的实际类型可以与缓存类型相同或不同。缓存类型仅用于区分不同的缓存存储。
然后,您可以决定使用“默认设置”允许缓存在执行终止后持续存在。
string bulkCache = ApplicationCaching<MyType>.ConvertCacheToString();
                //--------------------------------------------------------------------------------------------------------
                if (bulkCache != "")
                {
                    Properties.Settings.Default.*MyType*DataCachingStore = bulkCache;
                }
                //--------------------------------------------------------------------------------------------------------
                try
                {
                    Properties.Settings.Default.Save();
                }
                catch (IsolatedStorageException)
                {
                    //handle Isolated Storage exceptions here
                }


处理 InitialAccess 事件以在重新启动应用程序时重新初始化缓存。
private static void ApplicationCaching_InitialAccess(object sender, EventArgs e)
        {
            //-----------------------------------------------------------------------------------------
            string storedCache = Properties.Settings.Default.*MyType*DataCachingStore;
            ApplicationCaching<MyCacheType>.InializeCacheFromString(storedCache);
            //-----------------------------------------------------------------------------------------
        }

最后这里是Zipper类...

public class Zipper
    {
        public static void CopyTo(Stream src, Stream dest)
        {
            byte[] bytes = new byte[4096];

            int cnt;

            while ((cnt = src.Read(bytes, 0, bytes.Length)) != 0)
            {
                dest.Write(bytes, 0, cnt);
            }
        }

        public static byte[] Zip(string str)
        {
            var bytes = Encoding.UTF8.GetBytes(str);

            using (var msi = new MemoryStream(bytes))
            using (var mso = new MemoryStream())
            {
                using (var gs = new GZipStream(mso, CompressionMode.Compress))
                {
                    CopyTo(msi, gs);
                }
                return mso.ToArray();
            }
        }

        public static string Unzip(byte[] bytes)
        {
            using (var msi = new MemoryStream(bytes))
            using (var mso = new MemoryStream())
            {
                using (var gs = new GZipStream(msi, CompressionMode.Decompress))
                {
                    CopyTo(gs, mso);
                }
                return Encoding.UTF8.GetString(mso.ToArray());
            }
        }
    }


-1
如果您想在ASP.Net中缓存某些内容,那么我建议您查看Cache类。例如:
Hashtable menuTable = new Hashtable(); 
menuTable.add("Home","default.aspx"); 
Cache["menu"] = menuTable; 

然后再次检索它

Hashtable menuTable = (Hashtable)Cache["menu"];

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接