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:
- 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.
- 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.
- Skaliert nicht gut mit gleichzeitigen Schreibvorgängen.
- 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
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.
1 Stimmen
Dieser Artikel ist lesenswert: jondavis.net/techblog/post/2010/08/30/