38 Stimmen

Caching in C#/.Net

Ich wollte Sie fragen, was ist der beste Ansatz, um einen Cache in C# zu implementieren? Gibt es eine Möglichkeit, indem man bestimmte .NET-Klassen oder etwas Ähnliches verwendet? Vielleicht so etwas wie ein Wörterbuch, das einige Einträge entfernt, wenn es zu groß wird, aber wo deren Einträge nicht durch den Garbage Collector entfernt werden?

2 Stimmen

Es kommt darauf an schwer in der Anwendung. Wofür verwenden Sie es?

0 Stimmen

Nicht in einer asp.net Art und Weise, aber ich weiß noch nicht genau, ich werde die Anforderungen posten, wenn ich sie habe, aber danke für Ihre erste Antwort :)

0 Stimmen

Robustes .NET-Caching behandelt häufige Fallstricke des Caching und stellt eine Bibliothek zur Verfügung, die Entwicklern hilft, einige der üblichen Fallstricke zu vermeiden. Der Beitrag erklärt insbesondere, wie Sie SpeicherCache sicher.

2voto

Olivier Albertini Punkte 684

2voto

Alex Peck Punkte 4505

Wie bereits in anderen Antworten erwähnt, ist die Standardauswahl bei Verwendung des .NET Frameworks SpeicherCache und die verschiedenen zugehörigen Implementierungen in Microsoft NuGet-Paketen (z. B. Microsoft.Extensions.Caching.MemoryCache ). Alle diese Caches haben eine Größenbeschränkung in Bezug auf den verbrauchten Speicher und versuchen, den verbrauchten Speicher abzuschätzen, indem sie verfolgen, wie der gesamte physische Speicher im Verhältnis zur Anzahl der zwischengespeicherten Objekte wächst. Ein Hintergrund-Thread "trimmt" dann regelmäßig Einträge.

MemoryCache usw. haben einige Einschränkungen:

  1. Schlüssel sind Zeichenketten. Wenn der Schlüsseltyp also nicht von Haus aus eine Zeichenkette ist, sind Sie gezwungen, ständig Zeichenketten auf dem Heap zuzuweisen. Dies kann sich in einer Serveranwendung wirklich summieren, wenn Elemente "heiß" sind.
  2. Hat eine geringe "Scan-Resistenz" - z.B. wenn ein automatisierter Prozess schnell durch alle vorhandenen Elemente läuft, kann die Cache-Größe zu schnell wachsen, so dass der Hintergrund-Thread nicht mithalten kann. Dies kann zu Speicherdruck, Seitenfehlern, induziertem GC oder, wenn der Prozess unter IIS läuft, zum Recyceln des Prozesses aufgrund des Überschreitens des Limits für private Bytes führen.
  3. Skaliert nicht gut mit gleichzeitigen Schreibvorgängen.
  4. Enthält Perf-Counter, die nicht deaktiviert werden können (und somit Overhead verursachen).

Inwieweit diese Dinge problematisch sind, hängt von der Arbeitsbelastung ab. Ein alternativer Ansatz für die Zwischenspeicherung besteht darin, die Anzahl der Objekte im Cache zu begrenzen (anstatt den verwendeten Speicher zu schätzen). A Cache-Ersatzpolitik bestimmt dann, welches Objekt verworfen werden soll, wenn der Cache voll ist.

Nachfolgend finden Sie den Quellcode für einen einfachen Cache mit Least Recently Used Eviction Policy:

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; }
    }
}

Dies dient nur zur Veranschaulichung eines thread-sicheren Caches - er ist wahrscheinlich fehlerhaft und kann bei hoher gleichzeitiger Arbeitslast (z.B. in einem Webserver) ein Engpass sein.

Eine gründlich getestete, produktionsreife, skalierbare, gleichzeitige Implementierung geht ein wenig über einen Stack Overflow-Beitrag hinaus. Um dieses Problem in meinen Projekten zu lösen, implementierte ich eine Thread-sichere Pseudo-LRU (denken Sie nebenläufige Wörterbuch, aber mit eingeschränkter Größe). Die Leistung ist sehr nah an einem rohen ConcurrentDictionary, ~10x schneller als MemoryCache, ~10x besserer gleichzeitiger Durchsatz als ClassicLru oben, und bessere Trefferquote. Eine detaillierte Leistungsanalyse in der Github-Link unten zur Verfügung gestellt.

Die Verwendung sieht folgendermaßen aus:

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

0voto

Meysam Punkte 516

Ihre Frage bedarf einer weiteren Klärung. C# ist eine Sprache, kein Framework. Sie müssen angeben, welches Framework Sie für das Caching verwenden wollen. Wenn wir davon ausgehen, dass Sie es in ASP.NET implementieren wollen, hängt es immer noch ganz davon ab, was Sie von Cache erwarten. Sie können sich zwischen einem prozessinternen Cache (der die Daten im Heap Ihrer Anwendung hält) und einem prozessexternen Cache (in diesem Fall können Sie die Daten in einem anderen Speicher als dem Heap speichern, wie z. B. Amazon Elastic Cache Server) entscheiden. Und es gibt noch eine weitere Entscheidung zu treffen, nämlich die zwischen Client-Caching und Serve-Side-Caching. In der Regel müssen Sie verschiedene Lösungen für die Zwischenspeicherung verschiedener Daten entwickeln. Die Entscheidung, welche Lösung Sie benötigen, basiert auf vier Faktoren (Zugänglichkeit, Beständigkeit, Größe, Kosten).

0voto

Mark Punkte 83

Ich habe dies vor einiger Zeit geschrieben und es scheint gut zu funktionieren. Es ermöglicht Ihnen, verschiedene Cache-Speicher durch die Verwendung verschiedener Typen zu unterscheiden: ApplicationCaching<MyCacheType1>, ApplicationCaching<MyCacheType2>... .

Sie können festlegen, dass einige Speicher nach der Ausführung bestehen bleiben und andere verfallen.

Sie benötigen einen Verweis auf die Newtonsoft.Json Serializer (oder verwenden Sie einen alternativen) und natürlich müssen alle Objekte oder Wertetypen, die zwischengespeichert werden sollen, serialisierbar sein.

Utilice MaxItemCount um die Anzahl der Artikel in einer Filiale zu begrenzen.

Eine separate Zipper-Klasse (siehe Code unten) verwendet System.IO.Compression . Dadurch wird die Größe des Shops minimiert und die Ladezeiten werden verkürzt.

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);
            //-----------------------------------------------------------------------------------------
        }
        //=============================================================================================
    }

Sie können den Cache ganz einfach mit einer Funktion wie...

            //------------------------------------------------------------------------------------------------------------------------------
            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));
            }

Beachten Sie, dass die tatsächlich im Cache gespeicherten Typen mit dem Cachetyp übereinstimmen oder von ihm abweichen können. Der Cache-Typ wird NUR zur Unterscheidung der Cache-Speicher verwendet.

Sie können dann entscheiden, ob der Cache nach Beendigung der Ausführung erhalten bleiben soll, indem Sie Default Settings

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
                }

Behandeln Sie das InitialAccess-Ereignis, um den Cache neu zu initialisieren, wenn Sie die Anwendung neu starten.

private static void ApplicationCaching_InitialAccess(object sender, EventArgs e)
        {
            //-----------------------------------------------------------------------------------------
            string storedCache = Properties.Settings.Default.*MyType*DataCachingStore;
            ApplicationCaching<MyCacheType>.InializeCacheFromString(storedCache);
            //-----------------------------------------------------------------------------------------
        }

Hier ist endlich die Zipper-Klasse...

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());
            }
        }
    }

-1voto

uriDium Punkte 12771

Wenn Sie etwas in ASP.Net zwischenspeichern möchten, dann würde ich mir die Cache-Klasse ansehen. Zum Beispiel

Hashtable menuTable = new Hashtable(); 
menuTable.add("Home","default.aspx"); 
Cache["menu"] = menuTable; 

Um sie dann wieder abzurufen

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

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X