Шаблон блокировки для правильного использования .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
2
Я согласен с Дартом Вейдером ... Я бы подумал, что ты склоняешься ... ReaderWriterLockSlimНо я бы также использовал эту технику, чтобы избегать try-finallyзаявлений.
poy
1
Для вашей обновленной версии я бы больше не блокировал один cacheLock, вместо этого я бы блокировал каждый ключ. Это можно легко сделать с помощью, Dictionary<string, object>где ключ - это тот же ключ, который вы используете в своем, MemoryCacheа объект в словаре - это просто базовый объект, который вы используете Object. Однако, как говорится, я бы рекомендовал вам прочитать ответ Джона Ханна. Без надлежащего профилирования вы можете больше замедлить свою программу из-за блокировки, чем при SomeHeavyAndExpensiveCalculation()запуске двух экземпляров и отбрасывании одного результата.
Скотт Чемберлен
1
Мне кажется, что создание CacheItemPolicy после получения дорогостоящего значения для кеширования было бы более точным. В худшем случае, например при создании сводного отчета, который занимает 21 минуту, чтобы вернуть «дорогостоящую строку» (которая может содержать имя файла отчета в формате PDF), уже будет «просрочено» до того, как она будет возвращена.
Wonderbird
1
@Wonderbird Хороший замечание, я обновил свой ответ, чтобы сделать это.
Скотт Чемберлен

Ответы:

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;
    }
}

EDIT : приведенный ниже код не нужен, но я хотел оставить его, чтобы показать исходный метод. Это может быть полезно для будущих посетителей, которые используют другую коллекцию, которая имеет поточно-безопасное чтение, но не поточно-безопасную запись (почти все классы в 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, каким образом приведенный выше код не будет работать? Кроме того, это не строго «блокировка с двойной проверкой». Я просто следую аналогичному шаблону, и это был лучший способ описать его. Вот почему я сказал, что это что-то вроде блокировки с двойной проверкой.
Скотт Чемберлен
Я не комментировал ваш код. Я комментировал, что двойная проверка блокировки не работает. Ваш код в порядке.
DarthVader
1
Мне трудно понять, в каких ситуациях такая блокировка и такое хранилище будут иметь смысл: если вы блокируете все создания значений, MemoryCacheкоторые попадают в шансы, по крайней мере, одна из этих двух вещей была неправильной.
Джон Ханна
@ScottChamberlain просто смотрит на этот код, и разве он не подвержен возникновению исключения между получением блокировки и блоком try. Автор C # In a Nutshell обсуждает это здесь, albahari.com/threading/part2.aspx#_MonitorEnter_and_MonitorExit
BrutalSimplicity
9
Обратной стороной этого кода является то, что CacheKey «A» блокирует запрос к CacheKey «B», если оба они еще не кэшированы. Чтобы решить эту проблему, вы можете использовать concurrentDictionary <string, object>, в котором вы храните ключи кеша для блокировки
MichaelD
44

Существует библиотека с открытым исходным кодом [отказ от ответственности: я написал]: LazyCache, который IMO покрывает ваши требования двумя строками кода:

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

Он имеет встроенную блокировку по умолчанию, поэтому кэшируемый метод будет выполняться только один раз при промахе кеша, и он использует лямбда, поэтому вы можете выполнить «получить или добавить» за один раз. По умолчанию скользящее истечение 20 минут.

Есть даже пакет NuGet ;)

alastairtree
источник
4
Dapper кеширования.
Чарльз Бернс,
3
Это позволяет мне быть ленивым разработчиком, а это лучший ответ!
jdnew18
Стоит упомянуть статью, на которую указывает страница github для LazyCache, неплохо читается по причинам, стоящим за ней. alastaircrabtree.com/…
Rafael Merlin
2
Блокируется ли он по ключу или по кешу?
jjxtra
1
@DirkBoer, нет, его не заблокируют из-за того, как в lazycache используются блокировки и ленивый
alastairtree
30

Я решил эту проблему, применив метод AddOrGetExisting в MemoryCache и используя отложенную инициализацию .

По сути, мой код выглядит примерно так:

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
Скотт Вегнер,
2
Хотя это правда, что Lazy <T> может вернуть ошибку, если исключение инициализации не удалось, это довольно легко обнаружить. Затем вы можете удалить из кеша любой Lazy <T>, который разрешает ошибку, создать новый Lazy <T>, поместить его в кеш и разрешить его. В нашем собственном коде мы делаем нечто подобное. Мы повторим попытку заданное количество раз, прежде чем выдадим ошибку.
Кейт
12
AddOrGetExisting возвращает null, если элемент отсутствует, поэтому вам следует проверить и вернуть lazyObject в этом случае
Джан Марко,
1
Использование LazyThreadSafetyMode.PublicationOnly позволит избежать кэширования исключений.
Clement
2
Согласно комментариям в этом сообщении блога, если инициализировать запись кэша чрезвычайно дорого, лучше просто выселить исключение (как показано в примере в сообщении блога), чем использовать PublicationOnly, потому что существует вероятность того, что все потоки могут одновременно вызывать инициализатор.
bcr
15

Я предполагаю, что в этом коде есть проблемы с параллелизмом:

На самом деле, вполне возможно, что это нормально, хотя с возможным улучшением.

Теперь, в общем, шаблон, в котором у нас есть несколько потоков, устанавливающих общее значение при первом использовании, чтобы не блокировать получаемое и устанавливаемое значение, может быть следующим:

  1. Катастрофически - другой код предполагает, что существует только один экземпляр.
  2. Катастрофический - код, который получает экземпляр, не может выдерживать только одну (или, возможно, некоторое небольшое количество) одновременных операций.
  3. Катастрофический - средства хранения не являются потокобезопасными (например, если два потока добавляются в словарь, и вы можете получить всевозможные неприятные ошибки).
  4. Неоптимально - общая производительность хуже, чем если бы блокировка гарантировала, что только один поток выполняет работу по получению значения.
  5. Оптимально - затраты на то, чтобы несколько потоков выполняли избыточную работу, меньше затрат на ее предотвращение, тем более что это может произойти только в течение относительно короткого периода.

Однако, учитывая, что MemoryCacheзаписи могут быть выселены, тогда:

  1. Если иметь более одного экземпляра катастрофически, то MemoryCacheэто неправильный подход.
  2. Если вы должны предотвратить одновременное создание, вы должны сделать это в момент создания.
  3. MemoryCache является потокобезопасным с точки зрения доступа к этому объекту, так что здесь это не проблема.

Обе эти возможности, конечно же, должны быть продуманы, хотя единственный раз, когда существуют два экземпляра одной и той же строки, может быть проблемой, если вы делаете очень определенные оптимизации, которые здесь не применяются *.

Итак, у нас остались возможности:

  1. Дешевле избежать дублирования звонков на SomeHeavyAndExpensiveCalculation().
  2. Это дешевле, если не избежать дублирующих звонков SomeHeavyAndExpensiveCalculation().

И решить это может быть сложно (действительно, из тех вещей, где лучше профилировать, чем предполагать, что вы сможете это решить). Однако здесь стоит учесть, что наиболее очевидные способы блокировки при вставке предотвращают все добавления в кеш, включая те, которые не связаны.

Это означает, что если бы у нас было 50 потоков, пытающихся установить 50 различных значений, тогда нам придется заставить все 50 потоков ждать друг друга, даже если они даже не собирались выполнять одинаковые вычисления.

Таким образом, вам, вероятно, лучше будет иметь код, который у вас есть, чем код, который избегает состояния гонки, и если условие гонки является проблемой, вам, скорее всего, придется обработать это где-то в другом месте или понадобится другой стратегия кэширования, чем та, которая удаляет старые записи †.

Единственное, что я бы изменил, так это то, что я бы заменил вызов на Set()один на 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);

    }
fr0ga
источник
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();
    }
}
art24war
источник
Очень быстрый LazyCache :) Я написал этот код для репозиториев REST API.
art24war 01
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
Теодор Зулиас
Думаю, да, но все же обмен полезным опытом;)
Сэм Саарян
0

Сложно выбрать, какой из них лучше; lock или ReaderWriterLockSlim. Вам нужна реальная статистика чисел чтения и записи, соотношений и т. Д.

Но если вы считаете, что использование «блокировки» - правильный путь. Тогда вот другое решение для разных нужд. Я также включаю в код решение Аллана Сюй. Потому что оба могут понадобиться для разных нужд.

Вот требования, которые побудили меня к этому решению:

  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";
        }
    }

}
yvzman
источник