C# 生产质量线程安全的内存中 LRU 缓存已过期?

Jon*_*Rea 7 c# collections asp.net-mvc multithreading lru

这可能就像在棍子上求月亮一样;但是是否有“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);
}
Run Code Online (Sandbox Code Playgroud)
  • 我想要“GetOrAdd”,因为我希望这些操作是“原子的”——即避免围绕试图同时查询缓存的两个线程的竞争条件
  • 我想要 Create 函数,因为创建这些对象很昂贵(需要复杂的数据库访问)
  • 我需要过期,因为这些对象会在一段时间后过期

Microsoft 的最佳解决方案似乎是“System.Runtime.Caching.MemoryCache”,但它似乎有几个警告:

  • 它需要定期轮询缓存以遵守强加的内存限制。我的系统中不可能出现内存不足的情况。我读过这篇文章,这让我很担心:MemoryCache 不遵守配置中的内存限制
  • 它似乎只有“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;
    }
}
Run Code Online (Sandbox Code Playgroud)

或者也许在 Lazy < T > 周围有更好的东西,它确实允许只创建一次结果,但感觉就像一个黑客(缓存 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

其他想法:

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

Ale*_*eck 5

我实现了一个专为并发工作负载设计的线程安全伪 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);
Run Code Online (Sandbox Code Playgroud)

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

Install-Package BitFaster.Caching
Run Code Online (Sandbox Code Playgroud)

  • 我更新了缓存命中基准来测试 Microsoft.Extensions.Caching.Memory.MemoryCache,在我的测试中,它比 System.Runtime.Caching.MemoryCache 稍慢。此测试是 100% 缓存命中,因此永远无法很好地预测现实世界的性能(其中未命中的代价很高)。它只是为了给出原始速度的指标。无论如何,较新的类在顺序扫描和内存使用失控方面存在相同的问题。底层算法是相同的,并且具有相同的优点和缺点。 (2认同)