【问题标题】:C# Production-quality Thread-Safe in-memory LRU Cache with Expiry?C# 生产质量的线程安全内存 LRU 缓存过期?
【发布时间】:2015-08-09 23:24:23
【问题描述】:

这可能就像在棍子上要月亮一样;但是是否有一个“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 函数,因为创建这些对象的成本很高(需要复杂的数据库访问)
  • 我需要过期,因为这些对象会在一段时间后过期

Microsoft 的最佳解决方案似乎是“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 周围有更好的东西,它确实只允许创建一次结果,但感觉就像一个 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;
    }
}

其他想法:

【问题讨论】:

  • 试试this
  • 该项目已弃用。您指的是 CSharpTest.Net.Collections 的 LurchTable 吗? github.com/csharptest/CSharpTest.Net.Collections - 不支持 Expiry ...
  • 奇怪的是没有一个好的LRU缓存项目。我可能不得不自己开始一个:)
  • 您的GetOrAdd 方法与MemoryCache 上可用的GetSet 方法实现起来相当简单——您不必使用AddOrGetExisting。至于尊重内存限制,在 .NET 中严格意义上实现它实际上是不可能的,但除非您的系统上没有配置虚拟内存(这是一个坏主意,我怀疑是这种情况),否则实际上缓存会一旦达到高内存使用率,就会非常快速地驱逐项目,并且永远不会导致您出现内存不足错误。

标签: c# asp.net-mvc multithreading collections lru


【解决方案1】:

我实现了一个专为并发工作负载设计的线程安全伪 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

Install-Package BitFaster.Caching

【讨论】:

  • 我看到你和System.Runtime.Caching.MemoryCache做了比较。 BitFaster.CachingMicrosoft.Extensions.Caching.Memory.MemoryCache 相比如何?
  • AFAIK MemoryCache 系列类具有相似的性能特征(System.Runtime.Caching.MemoryCache、HttpRuntime.Cache、System.Web.Caching、MicrosoftExtensions.Caching.Memory.MemoryCache)。底层实现都非常相似。
  • Microsoft.Extensions.Caching.Memory.MemoryCache 较新,具有object 键,并提供更多优先级选项。我不知道它是否比旧的实现更有效,但 TBH 我的期望是它可能是!
  • 我更新了我的缓存命中基准以测试 Microsoft.Extensions.Caching.Memory.MemoryCache,在我的测试中它比 System.Runtime.Caching.MemoryCache 稍慢。该测试是 100% 的缓存命中,因此永远不能很好地预测现实世界的性能(其中未命中是昂贵的)。它只是为了给出原始速度的指标。无论如何,较新的类在顺序扫描和失控内存使用方面存在相同的问题。底层算法是一样的,各有优劣。
  • 感谢 Alex 花时间进行这些基准测试。我会牢记BitFaster.Caching 库,以防我需要快速的内存缓存实现!
猜你喜欢
  • 1970-01-01
  • 2011-06-17
  • 2019-02-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-12-16
  • 2015-12-06
相关资源
最近更新 更多