首页 > 解决方案 > 寻找一种在缓存时减少锁定的方法

问题描述

我正在使用下面的代码来缓存项目。这是非常基本的。

我遇到的问题是,每次它缓存一个项目时,部分代码都会锁定。因此,每小时大约有一百万件物品到达,这是一个问题。

我已经尝试为每个cacheKey创建一个静态锁对象字典,以便锁定是细粒度的,但这本身就成为管理它们到期等的问题......

有没有更好的方法来实现最小锁定?

private static readonly object cacheLock = new object();
public static T GetFromCache<T>(string cacheKey, Func<T> GetData) where T : class {

    // Returns null if the string does not exist, prevents a race condition
    // where the cache invalidates between the contains check and the retrieval.
    T cachedData = MemoryCache.Default.Get(cacheKey) as T;

    if (cachedData != null) {
        return cachedData;
    }

    lock (cacheLock) {
        // Check to see if anyone wrote to the cache while we where
        // waiting our turn to write the new value.
        cachedData = MemoryCache.Default.Get(cacheKey) as T;

        if (cachedData != null) {
            return cachedData;
        }

        // The value still did not exist so we now write it in to the cache.
        cachedData = GetData();

        MemoryCache.Default.Set(cacheKey, cachedData, new CacheItemPolicy(...));
        return cachedData;
    }
}

标签: c#.net.net-core

解决方案


我不知道如何MemoryCache.Default实施,或者您是否可以控制它。但总的来说,更喜欢在多线程环境中使用ConcurrentDictionaryover with lock。Dictionary

GetFromCache会变成

ConcurrentDictionary<string, T> cache = new ConcurrentDictionary<string, T>();
...
cache.GetOrAdd("someKey", (key) =>
{
  var data = PullDataFromDatabase(key);
  return data;
});

还有两件事需要注意。

到期

您可以定义一个类型,而不是保存T为字典的值

struct CacheItem<T>
{
    public T Item { get; set; }
    public DateTime Expiry { get; set; }
}

并将缓存存储为CacheItem具有定义到期时间的缓存。

cache.GetOrAdd("someKey", (key) =>
{
    var data = PullDataFromDatabase(key);
    return new CacheItem<T>() { Item = data, Expiry = DateTime.UtcNow.Add(TimeSpan.FromHours(1)) };
});

现在您可以在异步线程中实现过期。

Timer expirationTimer = new Timer(ExpireCache, null, 60000, 60000);
...
void ExpireCache(object state)
{
    var needToExpire = cache.Where(c => DateTime.UtcNow >= c.Value.Expiry).Select(c => c.Key);
    foreach (var key in needToExpire)
    {
        cache.TryRemove(key, out CacheItem<T> _);
    }
}

每分钟一次,您搜索所有需要过期的缓存条目,并将它们删除。

“锁定”

使用ConcurrentDictionary保证同时读/写不会损坏字典或引发异常。但是,您仍然可能会遇到两个同时读取导致您从数据库中获取数据两次的情况。

解决此问题的一个巧妙技巧是将字典的值包装为Lazy

ConcurrentDictionary<string, Lazy<CacheItem<T>>> cache = new ConcurrentDictionary<string, Lazy<CacheItem<T>>>();
...
var data = cache.GetOrData("someKey", key => new Lazy<CacheItem<T>>(() => 
{
    var data = PullDataFromDatabase(key);
    return new CacheItem<T>() { Item = data, Expiry = DateTime.UtcNow.Add(TimeSpan.FromHours(1)) };
})).Value;

解释

在同时请求的情况下,GetOrAdd您最终可能会多次调用“如果不在缓存中则从数据库获取”委托。但是,GetOrAdd最终将只使用委托返回的值之一,并且通过返回 a Lazy,您保证只有一个Lazy会被调用。


推荐阅读