带有过期时间的字典缓存

10
我想创建一个类来返回一个值。这个值将被缓存在一个字典对象中,持续时间为2分钟。在这2分钟内,我需要返回缓存的值,过了2分钟之后,字典缓存对象应该重新读取该值。我需要使用字典对象而不是内存缓存或其他东西,并且我应该在测试方法类中执行代码而不是Windows表单或WPF。
我不知道如何在测试方法中使字典对象在2分钟后过期。
public class CacheClass
{
   public string GetValue()
   {
      Dictionary<int, string> Cache = new Dictionary<int, string>();
      string value1 = "Lina";
      string value2 = "Jack";

      return "cached value";
   }
}

我会创建一个新类型(可能是CacheItem<T>),并将字典设置为Dictionary<int, CacheItem<T>>CacheItem<T>类型将保存T(无论值是什么)和一个DateTime,以跟踪值何时添加到缓存中。然后,您只需检查缓存的值是否仍然有效,然后返回它或确定需要重新评估它。 - itsme86
2
为什么不使用MemoryCache? 微软最近将MemoryCache移动到一个没有之前附带的命名空间中。https://msdn.microsoft.com/zh-cn/library/system.runtime.caching.memorycache(v=vs.110).aspx - mageos
我该如何创建一个Datetime来跟踪缓存值? - Daina Hodges
也许可以使用 DateTime.UtcNow? - Sir Rufo
@mageos 必须使用字典而不是 memorycach。 - Daina Hodges
GetValue() 如何测试任何东西? - paparazzo
4个回答

16
public class Cache<TKey, TValue>
{
    private readonly Dictionary<TKey, CacheItem<TValue>> _cache = new Dictionary<TKey, CacheItem<TValue>>();

    public void Store(TKey key, TValue value, TimeSpan expiresAfter)
    {
        _cache[key] = new CacheItem<TValue>(value, expiresAfter);
    }

    public TValue Get(TKey key)
    {
        if (!_cache.ContainsKey(key)) return default(TValue);
        var cached = _cache[key];
        if (DateTimeOffset.Now - cached.Created >= cached.ExpiresAfter)
        {
            _cache.Remove(key);
            return default(TValue);
        }
        return cached.Value;
    }
}

public class CacheItem<T>
{
    public CacheItem(T value, TimeSpan expiresAfter)
    {
        Value = value;
        ExpiresAfter = expiresAfter;
    }
    public T Value { get; }
    internal DateTimeOffset Created { get; } = DateTimeOffset.Now;
    internal TimeSpan ExpiresAfter { get; }
}

var cache = new Cache<int, string>();
cache.Store(1, "SomeString", TimeSpan.FromMinutes(2));

那么 测试方法 只会运行大约 2 分钟等待过期吗?我会向缓存类注入一个 IDateTimeService - Sir Rufo
3
你不希望单元测试运行两分钟。理想情况下最好有一些抽象,这样就不会对某个.Now属性产生强依赖性。但在这种情况下引入它将会极大地复杂化答案。在现实生活中,我可能只会使用小于50毫秒的缓存持续时间和Thread.Sleep(200)进行测试,类似这样的东西。 - Scott Hannen

2

你需要存储缓存中的存储时间。这样,你就可以测试该项是否过期。如果找不到该项或该项已过期,则调用resolvefunction。

以下是一个可以通过调用回调函数填充自身的缓存示例:

public class MyCache<TKey, TValue>
{
    // type to store datetime and a value
    private struct CacheItem
    {
        public DateTime RetreivedTime;
        public TValue Value;
    }

    // the dictionary
    private Dictionary<TKey, CacheItem> _cache = new Dictionary<TKey, CacheItem>();

    private TimeSpan _timeout;
    private Func<TKey, TValue> _resolveFunc;

    public MyCache(TimeSpan timeout, Func<TKey, TValue> resolveFunc)
    {
        // store to fields
        _timeout = timeout;
        _resolveFunc = resolveFunc;
    }

    public TValue this[TKey key]
    {
        get
        {
            CacheItem valueWithDateTime;

            // if nothing is found, you should resolve it by default
            var getNewValue = true;

            // lookup the key
            if (_cache.TryGetValue(key, out valueWithDateTime))
                // test if the value RetreivedTime was expired?
                getNewValue = valueWithDateTime.RetreivedTime.Add(_timeout) > DateTime.UtcNow;

            // was it found? or expired?
            if (getNewValue)
            {
                valueWithDateTime = new CacheItem { RetreivedTime = DateTime.UtcNow, Value = _resolveFunc(key) };
                _cache[key] = valueWithDateTime;
            }

            // return the value
            return valueWithDateTime.Value;
        }
    }

    // the cleanup method, you should call it sometimes...
    public void Cleanup()
    {
        var currentDateTime = DateTime.UtcNow;

        // ToArray prevents modifying an iterated collection.
        foreach (var keyValue in _cache.ToArray())
            if (keyValue.Value.RetreivedTime.Add(_timeout) < currentDateTime)
                _cache.Remove(keyValue.Key);
    }
}

缓存文本文件的示例:

class Program
{
    static string RetreiveFileContent(string filename)
    {
        if(!File.Exists(filename))
            return default(string);

        return File.ReadAllText(filename);
    }

    static void Main(string[] args)
    {
        var textFileCache = new MyCache<string, string>(TimeSpan.FromMinutes(2), RetreiveFileContent);

        var content = textFileCache["helloworld.txt"];

        // sometimes you need to cleanup old data.
        textFileCache.Cleanup();
    }
}

当然,您需要创建一些异常处理...


1
我会创建一个新的缓存项类型,该类型会记录值以及将项添加到缓存中的时间:
class CacheItem<T>
{
    public T Value { get; }
    public DateTime TimeAdded { get; }
    public bool IsValid => (DateTime.UtcNow - TimeAdded) < _timeToLive;

    private readonly TimeSpam _timeToLive = TimeSpan.FromMinutes(2);

    public CacheItem(T value)
    {
        Value = value;
        TimeAdded = DateTime.UtcNow;
    }
}

然后将你的缓存集合更改为 Dictionary<int, CacheItem<string>>

public string GetValue(int key)
{
    CacheItem<string> item;
    if (_cache.TryGetValue(key, out item) && item.IsValid)
        return item.Value;
    return null; // Signifies no entry in cache or entry has expired.
}

0

我认为你想要更简单的东西。根据你的解释,我无法理解你为什么需要一个字典或集合?因为你的 CacheClass 只有一个 GetValue 方法,并且项不是从外部添加的。

因此,尝试像这样的东西,也许可以给你一个起点;

public class CacheObject
{
    public string Value { get; set; }

    public DateTime CreationDate { get; set; }

    public CacheObject()
    {
        CreationDate = DateTime.Now;
    }
}

public static class CacheClass
{
    private static Dictionary<int,CacheObject> _cache = new Dictionary<int, CacheObject>();

    public static string GetValue()
    {
        if (_cache.Count == 0 || _cache[0].CreationDate.AddMinutes(2) < DateTime.Now)
        {
            var cache = new CacheObject
            {
                Value = GetValueFromSource()
            };
            _cache[0] = cache;
        }
        return _cache[0].Value;
    }

    public static string GetValueFromSource()
    {
        return "Jack";
    }
}

用法:

var str = CacheClass.GetValue();

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