【问题标题】:Looking for a way to do less locking while caching寻找一种在缓存时减少锁定的方法
【发布时间】:2019-03-14 04:22:43
【问题描述】:

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

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

我尝试为每个 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;
    }
}

【问题讨论】:

  • 您说您每小时有一百万个缓存请求,但是您多久创建一次新缓存?如果您只有 5 个缓存并且平均每 30 分钟清除一次,那么您的锁定基本上不会占您的开销。另一方面,如果您每 30 秒填充 10 个缓存,您的锁定策略将增加大量开销。您从不同的缓存中请求未填充的项目的频率也很重要。如果很多,你不应该这样做,如果不是,这不太可能是瓶颈。
  • @Servy Million new 项目在一小时内到达。因此将建立一个锁,获取项目并添加到缓存中。

标签: c# .net .net-core


【解决方案1】:

您可以考虑使用ReaderWriterLockSlim,只有在需要时才能获得写锁。

使用cacheLock.EnterReadLock();cacheLock.EnterWriteLock(); 应该会大大提高性能。

我给出的那个链接甚至有一个缓存的例子,正是你需要的,我在这里复制:

public class SynchronizedCache 
{
    private ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim();
    private Dictionary<int, string> innerCache = new Dictionary<int, string>();

    public int Count
    { get { return innerCache.Count; } }

    public string Read(int key)
    {
        cacheLock.EnterReadLock();
        try
        {
            return innerCache[key];
        }
        finally
        {
            cacheLock.ExitReadLock();
        }
    }

    public void Add(int key, string value)
    {
        cacheLock.EnterWriteLock();
        try
        {
            innerCache.Add(key, value);
        }
        finally
        {
            cacheLock.ExitWriteLock();
        }
    }

    public bool AddWithTimeout(int key, string value, int timeout)
    {
        if (cacheLock.TryEnterWriteLock(timeout))
        {
            try
            {
                innerCache.Add(key, value);
            }
            finally
            {
                cacheLock.ExitWriteLock();
            }
            return true;
        }
        else
        {
            return false;
        }
    }

    public AddOrUpdateStatus AddOrUpdate(int key, string value)
    {
        cacheLock.EnterUpgradeableReadLock();
        try
        {
            string result = null;
            if (innerCache.TryGetValue(key, out result))
            {
                if (result == value)
                {
                    return AddOrUpdateStatus.Unchanged;
                }
                else
                {
                    cacheLock.EnterWriteLock();
                    try
                    {
                        innerCache[key] = value;
                    }
                    finally
                    {
                        cacheLock.ExitWriteLock();
                    }
                    return AddOrUpdateStatus.Updated;
                }
            }
            else
            {
                cacheLock.EnterWriteLock();
                try
                {
                    innerCache.Add(key, value);
                }
                finally
                {
                    cacheLock.ExitWriteLock();
                }
                return AddOrUpdateStatus.Added;
            }
        }
        finally
        {
            cacheLock.ExitUpgradeableReadLock();
        }
    }

    public void Delete(int key)
    {
        cacheLock.EnterWriteLock();
        try
        {
            innerCache.Remove(key);
        }
        finally
        {
            cacheLock.ExitWriteLock();
        }
    }

    public enum AddOrUpdateStatus
    {
        Added,
        Updated,
        Unchanged
    };

    ~SynchronizedCache()
    {
       if (cacheLock != null) cacheLock.Dispose();
    }
}

【讨论】:

    【解决方案2】:

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

    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 最终将只使用委托返回的值之一,并且通过返回 Lazy,您保证只有一个 Lazy 将被调用。

    【讨论】:

    • 您现在永远不会过期任何东西,并且可能会多次加载缓存。 OP的代码没有问题。
    • Lazy 的技巧是有问题的(或者在其他实现中已经存在),因为如果PullFromDatabase 返回异常,它会将异常保存在缓存中。对不对?
    • @AngryHacker 你说得对,我还没想过。我想解决它的方法是在惰性委托中捕获异常,并在 catch 块中将缓存设置为空。然后,检查检查是否为空。如果是,则从缓存中删除该项目(并在需要时抛出异常)。
    猜你喜欢
    • 2021-02-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-30
    • 1970-01-01
    • 2013-12-01
    • 1970-01-01
    相关资源
    最近更新 更多