C#生产级别的线程安全内存LRU缓存与过期时间?

8
这可能有点难以实现,但是否存在一个“C#生产质量的线程安全内存LRU缓存并带有过期时间”? 或者是否有任何最佳实践想法来实现相同的功能?
(LRU是“最近最少使用”- http://en.wikipedia.org/wiki/Cache_algorithms#LRU
澄清一下:我想在ASP.Net MVC网站中支持一个具有以下接口的内存缓存:
public interface ICache
{
    T GetOrAdd<T>(string key, Func<T> create, TimeSpan timeToLive) where T : class;
    bool Remove(string key);
}
  • 我想要使用"GetOrAdd",因为我希望这些操作是"原子性"的——即避免两个线程同时查询缓存时出现竞争条件
  • 我想要使用Create函数,因为创建这些对象很昂贵(需要复杂的数据库访问)
  • 我需要过期时间,因为这些对象在一段时间后会过期

微软提供的最佳解决方案似乎是"System.Runtime.Caching.MemoryCache",但它似乎有一些注意事项:

  • 它需要定期轮询缓存以遵守所强加的内存限制。我不能让我的系统有任何可能耗尽内存的情况。我已经阅读了这篇文章,这让我感到担忧:MemoryCache does not obey memory limits in configuration
  • 它似乎只有"AddOrGetExisting"支持我的接口,它需要将构造的对象作为第二个参数——如果这个对象很难创建,那么预先构造它是否不会削弱缓存的优点呢?

代码大致如下:

public sealed class Cache : ICache
{
    private readonly MemoryCache _cache;

    public Cache()
    {
        _cache = MemoryCache.Default;
    }

    public T GetOrAdd<T>(string key, Func<T> create, TimeSpan timeToLive) where T : class
    {
        // This call kinda defeats the point of the cache ?!?
        var newValue = create();

        return _cache.AddOrGetExisting(key, newValue, DateTimeOffset.UtcNow + timeToLive) as T;
    }

    public bool Remove(string key)
    {
        _cache.Remove(key);
        return true;
    }
}

或者可能有更好的解决方案,比如使用Lazy < T >,它允许结果仅被创建一次,但感觉像是一个hack(缓存Func是否有后果?):

class Program
{
    static void Main(string[] args)
    {
        Func<Foo> creation = () =>
        {
            // Some expensive thing
            return new Foo();
        };

        Cache cache = new Cache();
        // Result 1 and 2 are correctly the same instance. Result 3 is correctly a new instance...
        var result1 = cache.GetOrAdd("myKey", creation, TimeSpan.FromMinutes(30));
        var result2 = cache.GetOrAdd("myKey", creation, TimeSpan.FromMinutes(30));
        var result3 = cache.GetOrAdd("myKey3", creation, TimeSpan.FromMinutes(30));

        return;
    }
}

public sealed class Foo
{
    private static int Counter = 0;
    private int Index = 0;

    public Foo()
    {
        Index = ++Counter;
    }
}

public sealed class Cache
{
    private readonly MemoryCache _cache;

    public Cache()
    {
        _cache = MemoryCache.Default;
    }

    public T GetOrAdd<T>(string key, Func<T> create, TimeSpan timeToLive) where T : class
    {
        var newValue = new Lazy<T>(create, LazyThreadSafetyMode.PublicationOnly);
        var value = (Lazy<T>)_cache.AddOrGetExisting(key, newValue, DateTimeOffset.UtcNow + timeToLive);
        return (value ?? newValue).Value;
    }

    public bool Remove(string key)
    {
        _cache.Remove(key);
        return true;
    }
}

其他想法:

  • 我也找到了这个实现,但它不允许您指定基于时间的过期:是否有IDictionary的任何LRU实现?
  • 也许有一个使用ReaderWriterLock的实现?
  • 一些包装ConcurrentDictionary的东西?

尝试使用此链接:https://www.nuget.org/packages/CSharpTest.Net.Library/ - Yuval Itzchakov
该项目已被弃用。您是指CSharpTest.Net.Collections的LurchTable吗?https://github.com/csharptest/CSharpTest.Net.Collections - 它不支持过期... - Jon Rea
2
很奇怪没有一个好的LRU缓存项目。也许我得自己开始一个 :) - Yuval Itzchakov
你的 GetOrAdd 方法可以使用 MemoryCache 上可用的 GetSet 方法来实现,而不必使用 AddOrGetExisting。至于尊重内存限制,在 .NET 中严格意义上实现这一点几乎是不可能的,但除非您的系统未配置虚拟内存(这是一个糟糕的想法,我怀疑这种情况会发生),在实践中,一旦达到高内存使用率,缓存将非常快地驱逐项目,并且不应该导致内存不足错误。 - Mike Marynowski
1个回答

9

我实现了一个针对并发工作负载的线程安全伪LRU,目前正在生产系统中使用。性能非常接近ConcurrentDictionary,比MemoryCache快约10倍,并且命中率优于传统的LRU。完整分析请参见下面的GitHub链接。

使用方法如下:

int capacity = 666;
var lru = new ConcurrentTLru<int, SomeItem>(capacity, TimeSpan.FromMinutes(5));

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

GitHub:https://github.com/bitfaster/BitFaster.Caching

这是一个指向 BitFaster.Caching 的 GitHub 页面的链接。
Install-Package BitFaster.Caching

我看到你已经将System.Runtime.Caching.MemoryCacheMicrosoft.Extensions.Caching.Memory.MemoryCache进行了比较。那么BitFaster.CachingMicrosoft.Extensions.Caching.Memory.MemoryCache相比如何? - Theodor Zoulias
据我所知,MemoryCache类族具有相似的性能特征(System.Runtime.Caching.MemoryCache、HttpRuntime.Cache、System.Web.Caching、MicrosoftExtensions.Caching.Memory.MemoryCache)。它们的底层实现都非常相似。 - Alex Peck
Microsoft.Extensions.Caching.Memory.MemoryCache 是较新的缓存实现,支持 object 键,并提供更多的优先级选项。我不确定它是否比旧实现更高效,但说实话,我的期望是它可能确实如此! - Theodor Zoulias
3
我更新了我的缓存命中率基准测试,以测试Microsoft.Extensions.Caching.Memory.MemoryCache,并且在我的测试中,它比System.Runtime.Caching.MemoryCache稍微慢一些。这个测试是100%的缓存命中,因此永远不会是真实世界性能的好预测器(其中未命中很昂贵)。它只是为了给出原始速度的指示。无论如何,较新的类具有与顺序扫描和失控内存使用相同的问题。底层算法是相同的,它具有相同的优点和缺点。 - Alex Peck
感谢Alex花时间制作这些基准测试。如果我需要快速的内存缓存实现,我会记住BitFaster.Caching库! - Theodor Zoulias

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