正确使用.NET MemoryCache的锁定模式


115

我认为这段代码存在并发问题:

const string CacheKey = "CacheKey";
static string GetCachedData()
{
    string expensiveString =null;
    if (MemoryCache.Default.Contains(CacheKey))
    {
        expensiveString = MemoryCache.Default[CacheKey] as string;
    }
    else
    {
        CacheItemPolicy cip = new CacheItemPolicy()
        {
            AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
        };
        expensiveString = SomeHeavyAndExpensiveCalculation();
        MemoryCache.Default.Set(CacheKey, expensiveString, cip);
    }
    return expensiveString;
}

并发问题的原因是多个线程可以获取空键,然后尝试将数据插入缓存。

进行此代码并发证明的最短,最干净的方法是什么?我喜欢在与缓存相关的代码中遵循良好的模式。链接到在线文章会很有帮助。

更新:

我根据@Scott Chamberlain的答案提出了此代码。有人能找到与此相关的任何性能或并发问题吗?如果可行,它将节省很多代码和错误。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.Caching;

namespace CachePoc
{
    class Program
    {
        static object everoneUseThisLockObject4CacheXYZ = new object();
        const string CacheXYZ = "CacheXYZ";
        static object everoneUseThisLockObject4CacheABC = new object();
        const string CacheABC = "CacheABC";

        static void Main(string[] args)
        {
            string xyzData = MemoryCacheHelper.GetCachedData<string>(CacheXYZ, everoneUseThisLockObject4CacheXYZ, 20, SomeHeavyAndExpensiveXYZCalculation);
            string abcData = MemoryCacheHelper.GetCachedData<string>(CacheABC, everoneUseThisLockObject4CacheXYZ, 20, SomeHeavyAndExpensiveXYZCalculation);
        }

        private static string SomeHeavyAndExpensiveXYZCalculation() {return "Expensive";}
        private static string SomeHeavyAndExpensiveABCCalculation() {return "Expensive";}

        public static class MemoryCacheHelper
        {
            public static T GetCachedData<T>(string cacheKey, object cacheLock, int cacheTimePolicyMinutes, 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 retreival.
                T cachedData = MemoryCache.Default.Get(cacheKey, null) 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, null) as T;

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

                    //The value still did not exist so we now write it in to the cache.
                    CacheItemPolicy cip = new CacheItemPolicy()
                    {
                        AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(cacheTimePolicyMinutes))
                    };
                    cachedData = GetData();
                    MemoryCache.Default.Set(cacheKey, cachedData, cip);
                    return cachedData;
                }
            }
        }
    }
}

3
你为什么不使用ReaderWriterLockSlim
DarthVader 2014年

2
我同意DarthVader的观点……我认为您是精瘦的ReaderWriterLockSlim……但我也将使用这种技术来避免try-finally陈述。
poy

1
对于您的更新版本,我不再锁定单个cacheLock,而是锁定每个键。可以很容易地做到这一点,Dictionary<string, object>其中的键与您使用的键相同MemoryCache,而字典中的对象只是Object您锁定的基本键。不过,话虽如此,我建议您通读乔恩·汉纳的回答。如果没有适当的配置文件,则锁定可能会使程序减慢速度,而不是让两个实例SomeHeavyAndExpensiveCalculation()运行并丢弃一个结果。
Scott Chamberlain 2014年

1
在我看来,在获取昂贵的值以进行缓存之后创建CacheItemPolicy会更加准确。在最坏的情况下(例如,创建需要21分钟才能返回“昂贵字符串”(可能包含PDF报告的文件名)的摘要报告),在返回之前就已经“过期”了。
Wonderbird 2014年

1
@Wonderbird好点,我更新了答案来做到这一点。
Scott Chamberlain

Answers:


91

这是我的第二次迭代代码。因为MemoryCache是线程安全的,所以您不需要锁定初始读取,因此只需读取即可,如果缓存返回null,则进行锁定检查以查看是否需要创建字符串。它极大地简化了代码。

const string CacheKey = "CacheKey";
static readonly object cacheLock = new object();
private static string GetCachedData()
{

    //Returns null if the string does not exist, prevents a race condition where the cache invalidates between the contains check and the retreival.
    var cachedString = MemoryCache.Default.Get(CacheKey, null) as string;

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

    lock (cacheLock)
    {
        //Check to see if anyone wrote to the cache while we where waiting our turn to write the new value.
        cachedString = MemoryCache.Default.Get(CacheKey, null) as string;

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

        //The value still did not exist so we now write it in to the cache.
        var expensiveString = SomeHeavyAndExpensiveCalculation();
        CacheItemPolicy cip = new CacheItemPolicy()
                              {
                                  AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
                              };
        MemoryCache.Default.Set(CacheKey, expensiveString, cip);
        return expensiveString;
    }
}

编辑:下面的代码是不必要的,但我想保留它以显示原始方法。对于正在使用具有线程安全读取但非线程安全写入(System.Collections名称空间下的几乎所有类都是这样)的其他集合的将来的访问者来说,这可能很有用。

这是我ReaderWriterLockSlim用来保护访问权限的方式。您需要执行一种“ 双重检查锁定 ”,以查看是否有其他人在我们等待锁的同时创建了缓存的项目。

const string CacheKey = "CacheKey";
static readonly ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim();
static string GetCachedData()
{
    //First we do a read lock to see if it already exists, this allows multiple readers at the same time.
    cacheLock.EnterReadLock();
    try
    {
        //Returns null if the string does not exist, prevents a race condition where the cache invalidates between the contains check and the retreival.
        var cachedString = MemoryCache.Default.Get(CacheKey, null) as string;

        if (cachedString != null)
        {
            return cachedString;
        }
    }
    finally
    {
        cacheLock.ExitReadLock();
    }

    //Only one UpgradeableReadLock can exist at one time, but it can co-exist with many ReadLocks
    cacheLock.EnterUpgradeableReadLock();
    try
    {
        //We need to check again to see if the string was created while we where waiting to enter the EnterUpgradeableReadLock
        var cachedString = MemoryCache.Default.Get(CacheKey, null) as string;

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

        //The entry still does not exist so we need to create it and enter the write lock
        var expensiveString = SomeHeavyAndExpensiveCalculation();
        cacheLock.EnterWriteLock(); //This will block till all the Readers flush.
        try
        {
            CacheItemPolicy cip = new CacheItemPolicy()
            {
                AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
            };
            MemoryCache.Default.Set(CacheKey, expensiveString, cip);
            return expensiveString;
        }
        finally 
        {
            cacheLock.ExitWriteLock();
        }
    }
    finally
    {
        cacheLock.ExitUpgradeableReadLock();
    }
}

1
@DarthVader上面的代码不能以什么方式工作?同样,这也不是严格的“双重检查锁定”,我只是遵循类似的模式,这是我想到的最好的描述方式。这就是为什么我说这是一种双重检查锁定。
Scott Chamberlain

我没有对您的代码发表评论。我评论说“双重检查锁定”无效。您的代码很好。
DarthVader 2014年

1
我发现很难看到这种锁定和这种存储在什么情况下会有意义:如果您锁定所有值的创建,MemoryCache那么这至少有两件事是错误的。
乔恩·汉娜

@ScottChamberlain只是看这段代码,并且不容易在获取锁和try块之间引发异常。C#的作者在这里对此进行
threading / part2.aspx

9
该代码的缺点是,如果CacheKey“ A”尚未缓存,则将阻止对CacheKey“ B”的请求。为了解决这个问题,您可以使用并发Dictionary <string,object>来存储要锁定的缓存键
MichaelD

44

有一个开源库[免责声明:我写的]:IMO用两行代码满足您的需求的LazyCache

IAppCache cache = new CachingService();
var cachedResults = cache.GetOrAdd("CacheKey", 
  () => SomeHeavyAndExpensiveCalculation());

它默认情况下内置锁定功能,因此可缓存方法在每次缓存未命中时仅执行一次,并且使用lambda,因此您可以一次性进行“获取或添加”操作。默认为20分钟滑动到期时间。

甚至还有一个NuGet包 ;)


4
缓存的Dapper。
查尔斯·伯恩斯

3
这使我成为一个懒惰的开发人员,这是最好的答案!
jdnew18

值得一提的是LazyCache的github页面指向的文章,由于其背后的原因,它是一本不错的书。alastaircrabtree.com/…–
拉斐尔·梅林

2
是按键还是按缓存锁定?
jjxtra

1
@DirkBoer没有也不会被阻塞的,因为这样的锁和懒惰在lazycache使用
alastairtree

30

我已经通过在MemoryCache上使用AddOrGetExisting方法和使用惰性初始化来解决了这个问题。

本质上,我的代码看起来像这样:

static string GetCachedData(string key, DateTimeOffset offset)
{
    Lazy<String> lazyObject = new Lazy<String>(() => SomeHeavyAndExpensiveCalculationThatReturnsAString());
    var returnedLazyObject = MemoryCache.Default.AddOrGetExisting(key, lazyObject, offset); 
    if (returnedLazyObject == null)
       return lazyObject.Value;
    return ((Lazy<String>) returnedLazyObject).Value;
}

最糟糕的情况是您Lazy两次创建相同的对象。但是,这很简单。使用AddOrGetExisting保证,您将只能获得该Lazy对象的一个实例,因此,还可以保证只调用一次昂贵的初始化方法。


4
这种方法的问题是您可以插入无效的数据。如果SomeHeavyAndExpensiveCalculationThatResultsAString()引发异常,则将其保留在缓存中。即使是暂时性的异常也将被缓存在Lazy<T>msdn.microsoft.com/en-us/library/vstudio/dd642331.aspx
Scott Wegner

2
Lazy <T>可以在初始化异常失败时返回错误,这确实很容易检测。然后,您可以从缓存中逐出任何可以解决错误的Lazy <T>,创建新的Lazy <T>,将其放入缓存中并进行解决。在我们自己的代码中,我们执行类似的操作。在抛出错误之前,我们将重试一定次数。
基思

12
如果该项目不存在,则AddOrGetExisting返回null,因此在这种情况下,您应检查并返回lazyObject
Gian Marco

1
使用LazyThreadSafetyMode.PublicationOnly将避免缓存异常。
克莱门特2015年

2
根据此博客文章中的评论,如果初始化缓存条目的成本非常高,那么最好驱逐一个异常(如博客文章中的示例所示),而不是使用PublicationOnly,因为所有线程可以同时调用初始化程序。
bcr

15

我认为这段代码存在并发问题:

实际上,尽管可能会有所改进,但可能还不错。

现在,通常,我们有多个线程在第一次使用时设置共享值,而不锁定获取和设置的值的模式可以是:

  1. 灾难性的-其他代码将假定仅存在一个实例。
  2. 灾难性的-获取实例的代码不仅不能容忍一个(或者可能是少数)并发操作。
  3. 灾难性的-存储方式不是线程安全的(例如,将两个线程添加到字典中,您可能会遇到各种讨厌的错误)。
  4. 次优-总体性能比锁确保只有一个线程完成获取值的工作要差。
  5. 最佳-使多个线程执行冗余工作的成本低于阻止它的成本,尤其是因为这只能在相对较短的时间内发生。

但是,在这里考虑MemoryCache可能会驱逐条目:

  1. 如果拥有多个实例造成灾难性后果,那MemoryCache是错误的方法。
  2. 如果必须阻止同时创建,则应在创建时进行。
  3. MemoryCache 就访问该对象而言,它是线程安全的,因此此处无需关注。

当然,这两种可能性都是必须考虑的,尽管唯一一次存在相同字符串的两个实例可能是一个问题,那就是您是否正在执行不适用于此处的非常特殊的优化*。

因此,我们剩下的可能性:

  1. 避免重复调用会比较便宜SomeHeavyAndExpensiveCalculation()
  2. 避免避免重复调用的费用会比较便宜SomeHeavyAndExpensiveCalculation()

解决这个问题可能很困难(实际上是值得进行概要分析而不是假设您可以解决的问题)。这里值得考虑的是,最明显的锁定插入方式将阻止对缓存的所有添加,包括无关的添加。

这意味着,如果我们有50个线程试图设置50个不同的值,那么即使它们甚至不打算执行相同的计算,也必须使所有50个线程彼此等待。

这样,您拥有的代码可能比避免竞争条件的代码更好,如果竞争条件是一个问题,您很可能需要在其他地方处理该问题,或者需要其他解决方案缓存策略要比淘汰旧条目的策略†。

我要更改的一件事是,我将用替换为Set()to AddOrGetExisting()。从上面可以明显看出,这可能不是必需的,但是它将允许收集新获得的项目,从而减少总体内存使用量,并允许较高的低代与高代收集比例。

所以是的,您可以使用双重锁定来防止并发,但是并发实际上不是问题,或者您以错误的方式存储值,或者在商店中双重锁定不是解决问题的最佳方法。

*如果您知道一组字符串中只有一个存在,则可以优化相等性比较,这大约是唯一一次具有两个字符串副本可能是错误的,而不仅仅是次优的,但是您想这样做完全不同的缓存类型才有意义。例如,排序XmlReader在内部进行。

†很可能要么是无限期存储的,要么是使用弱引用的,因此只有在没有现有用法的情况下,它才会删除条目。


1

为了避免全局锁定,您可以使用SingletonCache对每个键实现一个锁定,而不会增加内存使用量(不再引用锁定对象时将删除锁定对象,并且获取/释放是线程安全的,从而确保通过比较仅使用1个实例并交换)。

使用它看起来像这样:

SingletonCache<string, object> keyLocks = new SingletonCache<string, object>();

const string CacheKey = "CacheKey";
static string GetCachedData()
{
    string expensiveString =null;
    if (MemoryCache.Default.Contains(CacheKey))
    {
        return MemoryCache.Default[CacheKey] as string;
    }

    // double checked lock
    using (var lifetime = keyLocks.Acquire(url))
    {
        lock (lifetime.Value)
        {
           if (MemoryCache.Default.Contains(CacheKey))
           {
              return MemoryCache.Default[CacheKey] as string;
           }

           cacheItemPolicy cip = new CacheItemPolicy()
           {
              AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
           };
           expensiveString = SomeHeavyAndExpensiveCalculation();
           MemoryCache.Default.Set(CacheKey, expensiveString, cip);
           return expensiveString;
        }
    }      
}

代码在GitHub上:https : //github.com/bitfaster/BitFaster.Caching

Install-Package BitFaster.Caching

还有一个LRU实现,它的重量比MemoryCache轻,并且具有几个优点-更快的并发读写,有界大小,没有后台线程,内部性能计数器等。(免责声明,我写了它)。


0

控制台例子的MemoryCache,“如何保存/获取简单的类对象”

启动并按下后的输出, Any key除了Esc

正在保存以缓存!
从缓存中获取!
Some1
Some2

    class Some
    {
        public String text { get; set; }

        public Some(String text)
        {
            this.text = text;
        }

        public override string ToString()
        {
            return text;
        }
    }

    public static MemoryCache cache = new MemoryCache("cache");

    public static string cache_name = "mycache";

    static void Main(string[] args)
    {

        Some some1 = new Some("some1");
        Some some2 = new Some("some2");

        List<Some> list = new List<Some>();
        list.Add(some1);
        list.Add(some2);

        do {

            if (cache.Contains(cache_name))
            {
                Console.WriteLine("Getting from cache!");
                List<Some> list_c = cache.Get(cache_name) as List<Some>;
                foreach (Some s in list_c) Console.WriteLine(s);
            }
            else
            {
                Console.WriteLine("Saving to cache!");
                cache.Set(cache_name, list, DateTime.Now.AddMinutes(10));                   
            }

        } while (Console.ReadKey(true).Key != ConsoleKey.Escape);

    }

0
public interface ILazyCacheProvider : IAppCache
{
    /// <summary>
    /// Get data loaded - after allways throw cached result (even when data is older then needed) but very fast!
    /// </summary>
    /// <param name="key"></param>
    /// <param name="getData"></param>
    /// <param name="slidingExpiration"></param>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    T GetOrAddPermanent<T>(string key, Func<T> getData, TimeSpan slidingExpiration);
}

/// <summary>
/// Initialize LazyCache in runtime
/// </summary>
public class LazzyCacheProvider: CachingService, ILazyCacheProvider
{
    private readonly Logger _logger = LogManager.GetLogger("MemCashe");
    private readonly Hashtable _hash = new Hashtable();
    private readonly List<string>  _reloader = new List<string>();
    private readonly ConcurrentDictionary<string, DateTime> _lastLoad = new ConcurrentDictionary<string, DateTime>();  


    T ILazyCacheProvider.GetOrAddPermanent<T>(string dataKey, Func<T> getData, TimeSpan slidingExpiration)
    {
        var currentPrincipal = Thread.CurrentPrincipal;
        if (!ObjectCache.Contains(dataKey) && !_hash.Contains(dataKey))
        {
            _hash[dataKey] = null;
            _logger.Debug($"{dataKey} - first start");
            _lastLoad[dataKey] = DateTime.Now;
            _hash[dataKey] = ((object)GetOrAdd(dataKey, getData, slidingExpiration)).CloneObject();
            _lastLoad[dataKey] = DateTime.Now;
           _logger.Debug($"{dataKey} - first");
        }
        else
        {
            if ((!ObjectCache.Contains(dataKey) || _lastLoad[dataKey].AddMinutes(slidingExpiration.Minutes) < DateTime.Now) && _hash[dataKey] != null)
                Task.Run(() =>
                {
                    if (_reloader.Contains(dataKey)) return;
                    lock (_reloader)
                    {
                        if (ObjectCache.Contains(dataKey))
                        {
                            if(_lastLoad[dataKey].AddMinutes(slidingExpiration.Minutes) > DateTime.Now)
                                return;
                            _lastLoad[dataKey] = DateTime.Now;
                            Remove(dataKey);
                        }
                        _reloader.Add(dataKey);
                        Thread.CurrentPrincipal = currentPrincipal;
                        _logger.Debug($"{dataKey} - reload start");
                        _hash[dataKey] = ((object)GetOrAdd(dataKey, getData, slidingExpiration)).CloneObject();
                        _logger.Debug($"{dataKey} - reload");
                        _reloader.Remove(dataKey);
                    }
                });
        }
        if (_hash[dataKey] != null) return (T) (_hash[dataKey]);

        _logger.Debug($"{dataKey} - dummy start");
        var data = GetOrAdd(dataKey, getData, slidingExpiration);
        _logger.Debug($"{dataKey} - dummy");
        return (T)((object)data).CloneObject();
    }
}

非常快的LazyCache :)我为REST API存储库编写了此代码。
art24war

0

有点晚了,但是...完整实现:

    [HttpGet]
    public async Task<HttpResponseMessage> GetPageFromUriOrBody(RequestQuery requestQuery)
    {
        log(nameof(GetPageFromUriOrBody), nameof(requestQuery));
        var responseResult = await _requestQueryCache.GetOrCreate(
            nameof(GetPageFromUriOrBody)
            , requestQuery
            , (x) => getPageContent(x).Result);
        return Request.CreateResponse(System.Net.HttpStatusCode.Accepted, responseResult);
    }
    static MemoryCacheWithPolicy<RequestQuery, string> _requestQueryCache = new MemoryCacheWithPolicy<RequestQuery, string>();

这是getPageContent签名:

async Task<string> getPageContent(RequestQuery requestQuery);

这是MemoryCacheWithPolicy实现:

public class MemoryCacheWithPolicy<TParameter, TResult>
{
    static ILogger _nlogger = new AppLogger().Logger;
    private MemoryCache _cache = new MemoryCache(new MemoryCacheOptions() 
    {
        //Size limit amount: this is actually a memory size limit value!
        SizeLimit = 1024 
    });

    /// <summary>
    /// Gets or creates a new memory cache record for a main data
    /// along with parameter data that is assocciated with main main.
    /// </summary>
    /// <param name="key">Main data cache memory key.</param>
    /// <param name="param">Parameter model that assocciated to main model (request result).</param>
    /// <param name="createCacheData">A delegate to create a new main data to cache.</param>
    /// <returns></returns>
    public async Task<TResult> GetOrCreate(object key, TParameter param, Func<TParameter, TResult> createCacheData)
    {
        // this key is used for param cache memory.
        var paramKey = key + nameof(param);

        if (!_cache.TryGetValue(key, out TResult cacheEntry))
        {
            // key is not in the cache, create data through the delegate.
            cacheEntry = createCacheData(param);
            createMemoryCache(key, cacheEntry, paramKey, param);

            _nlogger.Warn(" cache is created.");
        }
        else
        {
            // data is chached so far..., check if param model is same (or changed)?
            if(!_cache.TryGetValue(paramKey, out TParameter cacheParam))
            {
                //exception: this case should not happened!
            }

            if (!cacheParam.Equals(param))
            {
                // request param is changed, create data through the delegate.
                cacheEntry = createCacheData(param);
                createMemoryCache(key, cacheEntry, paramKey, param);
                _nlogger.Warn(" cache is re-created (param model has been changed).");
            }
            else
            {
                _nlogger.Trace(" cache is used.");
            }

        }
        return await Task.FromResult<TResult>(cacheEntry);
    }
    MemoryCacheEntryOptions createMemoryCacheEntryOptions(TimeSpan slidingOffset, TimeSpan relativeOffset)
    {
        // Cache data within [slidingOffset] seconds, 
        // request new result after [relativeOffset] seconds.
        return new MemoryCacheEntryOptions()

            // Size amount: this is actually an entry count per 
            // key limit value! not an actual memory size value!
            .SetSize(1)

            // Priority on removing when reaching size limit (memory pressure)
            .SetPriority(CacheItemPriority.High)

            // Keep in cache for this amount of time, reset it if accessed.
            .SetSlidingExpiration(slidingOffset)

            // Remove from cache after this time, regardless of sliding expiration
            .SetAbsoluteExpiration(relativeOffset);
        //
    }
    void createMemoryCache(object key, TResult cacheEntry, object paramKey, TParameter param)
    {
        // Cache data within 2 seconds, 
        // request new result after 5 seconds.
        var cacheEntryOptions = createMemoryCacheEntryOptions(
            TimeSpan.FromSeconds(2)
            , TimeSpan.FromSeconds(5));

        // Save data in cache.
        _cache.Set(key, cacheEntry, cacheEntryOptions);

        // Save param in cache.
        _cache.Set(paramKey, param, cacheEntryOptions);
    }
    void checkCacheEntry<T>(object key, string name)
    {
        _cache.TryGetValue(key, out T value);
        _nlogger.Fatal("Key: {0}, Name: {1}, Value: {2}", key, name, value);
    }
}

nlogger只是nLog跟踪MemoryCacheWithPolicy行为的对象。如果RequestQuery requestQuery通过委托(Func<TParameter, TResult> createCacheData)更改了请求对象(),或者在滑动时间或绝对时间达到其限制时重新创建,我将重新创建内存缓存。注意,一切也都是异步的;)


也许您的答案与以下问题更相关:从MemoryCache中获取异步线程安全
Theodor Zoulias,

我猜是这样,但仍然有用的经验交流;)
Sam Saarian

0

很难选择哪一个更好。锁或ReaderWriterLockSlim。您需要真实的读写数量和比率等统计数据。

但是如果您相信使用“锁定”是正确的方法。然后这是针对不同需求的不同解决方案。我还将在代码中包括Allan Xu的解决方案。因为两者可能因不同的需求而需要。

以下是促使我采用此解决方案的要求:

  1. 由于某些原因,您不想或不能提供“ GetData”功能。也许“ GetData”函数位于其他具有大量构造函数的类中,并且您甚至不想创建一个实例,直到确保它无法逃脱。
  2. 您需要从应用程序的不同位置/层访问相同的缓存数据。而且这些不同的位置无法访问同一储物柜对象。
  3. 您没有恒定的缓存键。例如; 需要使用sessionId缓存键缓存某些数据。

码:

using System;
using System.Runtime.Caching;
using System.Collections.Concurrent;
using System.Collections.Generic;

namespace CachePoc
{
    class Program
    {
        static object everoneUseThisLockObject4CacheXYZ = new object();
        const string CacheXYZ = "CacheXYZ";
        static object everoneUseThisLockObject4CacheABC = new object();
        const string CacheABC = "CacheABC";

        static void Main(string[] args)
        {
            //Allan Xu's usage
            string xyzData = MemoryCacheHelper.GetCachedDataOrAdd<string>(CacheXYZ, everoneUseThisLockObject4CacheXYZ, 20, SomeHeavyAndExpensiveXYZCalculation);
            string abcData = MemoryCacheHelper.GetCachedDataOrAdd<string>(CacheABC, everoneUseThisLockObject4CacheXYZ, 20, SomeHeavyAndExpensiveXYZCalculation);

            //My usage
            string sessionId = System.Web.HttpContext.Current.Session["CurrentUser.SessionId"].ToString();
            string yvz = MemoryCacheHelper.GetCachedData<string>(sessionId);
            if (string.IsNullOrWhiteSpace(yvz))
            {
                object locker = MemoryCacheHelper.GetLocker(sessionId);
                lock (locker)
                {
                    yvz = MemoryCacheHelper.GetCachedData<string>(sessionId);
                    if (string.IsNullOrWhiteSpace(yvz))
                    {
                        DatabaseRepositoryWithHeavyConstructorOverHead dbRepo = new DatabaseRepositoryWithHeavyConstructorOverHead();
                        yvz = dbRepo.GetDataExpensiveDataForSession(sessionId);
                        MemoryCacheHelper.AddDataToCache(sessionId, yvz, 5);
                    }
                }
            }
        }


        private static string SomeHeavyAndExpensiveXYZCalculation() { return "Expensive"; }
        private static string SomeHeavyAndExpensiveABCCalculation() { return "Expensive"; }

        public static class MemoryCacheHelper
        {
            //Allan Xu's solution
            public static T GetCachedDataOrAdd<T>(string cacheKey, object cacheLock, int minutesToExpire, 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 retreival.
                T cachedData = MemoryCache.Default.Get(cacheKey, null) 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, null) as T;

                    if (cachedData != null)
                        return cachedData;

                    cachedData = GetData();
                    MemoryCache.Default.Set(cacheKey, cachedData, DateTime.Now.AddMinutes(minutesToExpire));
                    return cachedData;
                }
            }

            #region "My Solution"

            readonly static ConcurrentDictionary<string, object> Lockers = new ConcurrentDictionary<string, object>();
            public static object GetLocker(string cacheKey)
            {
                CleanupLockers();

                return Lockers.GetOrAdd(cacheKey, item => (cacheKey, new object()));
            }

            public static T GetCachedData<T>(string cacheKey) where T : class
            {
                CleanupLockers();

                T cachedData = MemoryCache.Default.Get(cacheKey) as T;
                return cachedData;
            }

            public static void AddDataToCache(string cacheKey, object value, int cacheTimePolicyMinutes)
            {
                CleanupLockers();

                MemoryCache.Default.Add(cacheKey, value, DateTimeOffset.Now.AddMinutes(cacheTimePolicyMinutes));
            }

            static DateTimeOffset lastCleanUpTime = DateTimeOffset.MinValue;
            static void CleanupLockers()
            {
                if (DateTimeOffset.Now.Subtract(lastCleanUpTime).TotalMinutes > 1)
                {
                    lock (Lockers)//maybe a better locker is needed?
                    {
                        try//bypass exceptions
                        {
                            List<string> lockersToRemove = new List<string>();
                            foreach (var locker in Lockers)
                            {
                                if (!MemoryCache.Default.Contains(locker.Key))
                                    lockersToRemove.Add(locker.Key);
                            }

                            object dummy;
                            foreach (string lockerKey in lockersToRemove)
                                Lockers.TryRemove(lockerKey, out dummy);

                            lastCleanUpTime = DateTimeOffset.Now;
                        }
                        catch (Exception)
                        { }
                    }
                }

            }
            #endregion
        }
    }

    class DatabaseRepositoryWithHeavyConstructorOverHead
    {
        internal string GetDataExpensiveDataForSession(string sessionId)
        {
            return "Expensive data from database";
        }
    }

}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.