在 .Net 中使用 ObjectCache 缓存对象并过期

Cache object with ObjectCache in .Net with expiry time

我陷入了一个场景。 我的代码如下:

更新:它与如何使用数据缓存无关,我已经在使用它及其工作,它是关于扩展它的,因此该方法不会在过期时间和获取新的时间之间进行调用来自外部来源的数据

object = (string)this.GetDataFromCache(cache, cacheKey);

if(String.IsNullOrEmpty(object))
{
  // get the data. It takes 100ms
  SetDataIntoCache(cache, cacheKey, object, DateTime.Now.AddMilliseconds(500));
}

因此,如果项目过期,用户会调用缓存并从中获取数据,并从服务中获取数据并保存,以防万一,问题是,每当有待处理的请求时(请求进行中)服务发送另一个请求,因为对象已过期。最后应该有最多 2-3 次调用/秒,每秒有 10-20 次调用外部服务。

是否有任何最佳方法可以使请求时间之间没有冲突,然后使用数组和时间戳等创建自己的自定义 class?

顺便说一句,缓存的保存代码是-

private void SetDataIntoCache(ObjectCache cacheStore, string cacheKey, object target, DateTime slidingExpirationDuration)
{
  CacheItemPolicy cacheItemPolicy = new CacheItemPolicy();

  cacheItemPolicy.AbsoluteExpiration = slidingExpirationDuration;
  cacheStore.Add(cacheKey, target, cacheItemPolicy);
}

您将必须使用锁定来确保当缓存过期并且另一个线程正在从 remote/slow 服务获取请求时不发送请求,它看起来像这样(有更好的实现更易于使用,但它们需要单独的 类):

private static readonly object _Lock = new object();

...

object = (string)this.GetDataFromCache(cache, cacheKey);

if(object == null)
{
   lock(_Lock)
   {
        object = (string)this.GetDataFromCache(cache, cacheKey);
        if(String.IsNullOrEmpty(object))
        {
           get the data // take 100ms
           SetDataIntoCache(cache, cacheKey, object, DateTime.Now.AddMilliseconds(500));
        }
   }
}

return object;

此外,您要确保您的服务不会 return null,因为它会假设不存在缓存,并会尝试在每次请求时获取数据。这就是为什么更高级的实现通常使用诸如 CacheObject 之类的东西,它支持空值存储。

使用Double-checked locking模式:

var cachedItem = (string)this.GetDataFromCache(cache, cacheKey);
if (String.IsNullOrEmpty(object)) { // if no cache yet, or is expired
   lock (_lock) { // we lock only in this case
      // you have to make one more check, another thread might have put item in cache already
      cachedItem = (string)this.GetDataFromCache(cache, cacheKey); 
      if (String.IsNullOrEmpty(object)) {
          //get the data. take 100ms
          SetDataIntoCache(cache, cacheKey, cachedItem, DateTime.Now.AddMilliseconds(500));
      }
   }
}

这样,当您的缓存中有一个项目(因此,尚未过期)时,所有请求都将在不锁定的情况下完成。但是如果还没有缓存条目,或者它已过期 - 只有一个线程会获取数据并将其放入缓存中。 确保您了解该模式,因为在 .NET 中实现它时有一些注意事项。

如评论中所述,没有必要使用一个 "global" 锁对象来保护每个缓存访问。假设您的代码中有两个方法,并且每个方法都使用自己的缓存键(但仍使用相同的缓存)来缓存对象。然后你必须使用两个单独的锁对象,因为如果你将使用一个 "global" 锁对象,对一个方法的调用将不必要地等待对另一个方法的调用,而它们永远不会使用相同的缓存键。

顺便说一下,500 毫秒对于缓存来说太短了,你最终会在 add/remove 缓存中花费很多 CPU 循环,这最终会在任何其他请求可以之前过早地删除缓存获得缓存的好处。您应该剖析您的代码以查看它是否真正受益。

请记住,缓存在锁定、散列和许多其他数据移动方面有很多代码,这会花费大量 CPU 个周期,请记住,尽管 CPU 个周期很小,但是在多线程、多连接服务器中,CPU 还有很多其他事情要做。

原答案

private string GetDataFromCache(
            ObjectCache cache, 
            string key, 
            Func<string> valueFactory)
{
    var newValue = new Lazy<string>(valueFactory);            

    //The line below returns existing item or adds 
    // the new value if it doesn't exist
    var value = cache.AddOrGetExisting(key, newValue, DateTimeOffset.Now.AddMilliseconds(500)) as Lazy<string>;
    // Lazy<T> handles the locking itself
    return (value ?? newValue).Value;
}


// usage...


object = this.GetDataFromCache(cache, cacheKey, () => {

      // get the data...

      // this method will be called only once..

      // Lazy will automatically do necessary locking
      return data;
});

我已经采用了 Micro Caching in .NET for use with the System.Runtime.Caching.ObjectCache for MvcSiteMapProvider 的解决方案。完整的实现有一个 ICacheProvider 接口,允许在 System.Runtime.CachingSystem.Web.Caching 之间交换,但这是一个应该满足您需要的简化版本。

这个模式最引人注目的特点是它使用了一个轻量级的惰性锁版本来确保在缓存过期后只从数据源加载数据 1 次,而不管有多少并发线程正在尝试加载数据。

using System;
using System.Runtime.Caching;
using System.Threading;

public interface IMicroCache<T>
{
    bool Contains(string key);
    T GetOrAdd(string key, Func<T> loadFunction, Func<CacheItemPolicy> getCacheItemPolicyFunction);
    void Remove(string key);
}

public class MicroCache<T> : IMicroCache<T>
{
    public MicroCache(ObjectCache objectCache)
    {
        if (objectCache == null)
            throw new ArgumentNullException("objectCache");

        this.cache = objectCache;
    }
    private readonly ObjectCache cache;
    private ReaderWriterLockSlim synclock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);

    public bool Contains(string key)
    {
        synclock.EnterReadLock();
        try
        {
            return this.cache.Contains(key);
        }
        finally
        {
            synclock.ExitReadLock();
        }
    }

    public T GetOrAdd(string key, Func<T> loadFunction, Func<CacheItemPolicy> getCacheItemPolicyFunction)
    {
        LazyLock<T> lazy;
        bool success;

        synclock.EnterReadLock();
        try
        {
            success = this.TryGetValue(key, out lazy);
        }
        finally
        {
            synclock.ExitReadLock();
        }

        if (!success)
        {
            synclock.EnterWriteLock();
            try
            {
                if (!this.TryGetValue(key, out lazy))
                {
                    lazy = new LazyLock<T>();
                    var policy = getCacheItemPolicyFunction();
                    this.cache.Add(key, lazy, policy);
                }
            }
            finally
            {
                synclock.ExitWriteLock();
            }
        }

        return lazy.Get(loadFunction);
    }

    public void Remove(string key)
    {
        synclock.EnterWriteLock();
        try
        {
            this.cache.Remove(key);
        }
        finally
        {
            synclock.ExitWriteLock();
        }
    }


    private bool TryGetValue(string key, out LazyLock<T> value)
    {
        value = (LazyLock<T>)this.cache.Get(key);
        if (value != null)
        {
            return true;
        }
        return false;
    }

    private sealed class LazyLock<T>
    {
        private volatile bool got;
        private T value;

        public T Get(Func<T> activator)
        {
            if (!got)
            {
                if (activator == null)
                {
                    return default(T);
                }

                lock (this)
                {
                    if (!got)
                    {
                        value = activator();

                        got = true;
                    }
                }
            }

            return value;
        }
    }
}

用法

// Load the cache as a static singleton so all of the threads
// use the same instance.
private static IMicroCache<string> stringCache = 
    new MicroCache<string>(System.Runtime.Caching.MemoryCache.Default);

public string GetData(string key)
{
    return stringCache.GetOrAdd(
        key,
        () => LoadData(key),
        () => LoadCacheItemPolicy(key));
}

private string LoadData(string key)
{
    // Load data from persistent source here

    return "some loaded string";
}

private CacheItemPolicy LoadCacheItemPolicy(string key)
{
    var policy = new CacheItemPolicy();

    // This ensures the cache will survive application
    // pool restarts in ASP.NET/MVC
    policy.Priority = CacheItemPriority.NotRemovable;

    policy.AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(1);

    // Load Dependencies
    // policy.ChangeMonitors.Add(new HostFileChangeMonitor(new string[] { fileName }));

    return policy;
}

NOTE: As was previously mentioned, you are probably not gaining anything by caching a value that takes 100ms to retrieve for only 500ms. You should most likely choose a longer time period to hold items in the cache. Are the items really that volatile in the data source that they could change that quickly? If so, maybe you should look at using a ChangeMonitor to invalidate any stale data so you don't spend so much of the CPU time loading the cache. Then you can change the cache time to minutes instead of milliseconds.