Шаблон блокування для правильного використання .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безпечна нитка, вам не потрібно блокувати початкове зчитування, ви можете просто прочитати, і якщо кеш повернеться до нуля, тоді перевірте блокування, щоб побачити, чи потрібно створювати рядок. Це значно спрощує код.

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 просто дивиться на цей код, і чи не він сприйнятливий до винятку, який перекидається між придбанням блокування та блоком спробу. Автор C # In Nutshell обговорює це тут, albahari.com/threading/part2.aspx#_MonitorEnter_and_MonitorExit
BrutalSimplicity

9
Недоліком цього коду є те, що CacheKey "A" заблокує запит до CacheKey "B", якщо вони ще не кешовані. Для вирішення цього питання ви можете скористатися паралельним словником <string, object>, в якому ви зберігаєте кеш-ключі для блокування
MichaelD

44

Існує бібліотека з відкритим кодом [disclaimer: що я написав]: LazyCache, що IMO покриває вашу вимогу двома рядками коду:

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

За замовчуванням він має вбудоване блокування, тому метод кешування виконується лише один раз за один пропуск кешу, і він використовує лямбда, щоб ви могли "отримати або додати" за один раз. За замовчуванням ковзання закінчується 20 хвилин.

Є навіть пакет NuGet ;)


4
Dapper кешування.
Чарльз Бернс

3
Це дозволяє мені бути ледачим розробником, що робить це найкращою відповіддю!
jdnew18

Варто згадати статтю, на яку вказує сторінка github для LazyCache, досить добре прочитати з-за причин. alastaircrabtree.com/…
Рафаель Мерлін

2
Чи блокується він за ключем або за кешем?
jjxtra

1
@DirkBoer ні, його не заблокують через те, як замки та ледачі використовуються у lazycache
alastairtree

30

Я вирішив цю проблему, використовуючи метод AddOrGetExisting на MemoryCache та використовуючи ініціалізацію Lazy .

По суті, мій код виглядає приблизно так:

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 return null, якщо елемента не було, тому слід перевірити і повернути lazyObject у такому випадку
Gian Marco

1
Використання LazyThreadSafetyMode.PublicationOnly дозволить уникнути кешування винятків.
Климент

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

    }

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) або відтворюю, коли ковзання або абсолютний час досягають своєї межі. Зауважте, що все також є асинхронним;)


Можливо, ваша відповідь більше пов'язана з цим питанням: Async threadsafe Отримайте від MemoryCache
Theodor Zoulias

Я думаю, що так, але все ж корисний обмін досвідом;)
Сем Сааріан

0

Важко вибрати, хто з них кращий; замок або 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";
        }
    }

}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.