Який найкращий спосіб заблокувати кеш у asp.net?


80

Я знаю, що за певних обставин, таких як тривалі процеси, важливо заблокувати кеш ASP.NET, щоб уникнути подальших запитів іншого користувача щодо цього ресурсу від повторного виконання довгого процесу, замість того, щоб потрапити в кеш.

Який найкращий спосіб в c # застосувати блокування кешу в ASP.NET?

Відповіді:


114

Ось основний шаблон:

  • Перевірте значення в кеші, поверніть, якщо воно доступне
  • Якщо значення відсутнє в кеш-пам’яті, застосуйте блокування
  • Усередині блокування перевірте кеш ще раз, можливо, вас заблокували
  • Виконайте пошук значень та кешуйте його
  • Відпустіть замок

У коді це виглядає так:

private static object ThisLock = new object();

public string GetFoo()
{

  // try to pull from cache here

  lock (ThisLock)
  {
    // cache was empty before we got the lock, check again inside the lock

    // cache is still empty, so retreive the value here

    // store the value in the cache here
  }

  // return the cached value here

}

4
Якщо перше завантаження кеш-пам'яті триває кілька хвилин, чи все-таки є спосіб отримати доступ до вже завантажених записів? Скажімо, якщо у мене є GetFoo_AmazonArticlesByCategory (рядок categoryKey). Я думаю, це було б щось на зразок замка на категоріюКлюч.
Mathias F

5
Викликається "перевірена блокування". en.wikipedia.org/wiki/Double- Check_Locking
Brad Gagne

32

Для повноти повний приклад буде виглядати приблизно так.

private static object ThisLock = new object();
...
object dataObject = Cache["globalData"];
if( dataObject == null )
{
    lock( ThisLock )
    {
        dataObject = Cache["globalData"];

        if( dataObject == null )
        {
            //Get Data from db
             dataObject = GlobalObj.GetData();
             Cache["globalData"] = dataObject;
        }
    }
}
return dataObject;

7
if (dataObject == null) {lock (ThisLock) {if (dataObject == null) // звичайно це все ще нуль!
Константин

30
@Constantin: насправді, можливо, хтось оновив кеш, поки ви чекали на отримання блокування ()
Tudor Olariu

12
@ Джон Оуен - після оператора блокування вам доведеться спробувати дістати об'єкт з кешу ще раз!
Павло Ніколов

3
-1, код помилковий (прочитайте інші коментарі), чому ви не виправите це? Люди можуть спробувати використати ваш приклад.
orip

11
Цей код насправді все ще помилковий. Ви повертаєтеся globalObjectв області, де вона насправді не існує. Що повинно статися, так це те, що dataObjectслід використовувати всередині остаточної перевірки на нуль, а подія globalObject взагалі не повинна існувати.
Скотт Андерсон,

22

Не потрібно блокувати весь екземпляр кеш-пам'яті, навпаки, нам потрібно лише заблокувати конкретний ключ, для якого ви вставляєте. Тобто Не потрібно блокувати доступ до жіночого туалету, поки ви користуєтеся чоловічим туалетом :)

Реалізація нижче дозволяє блокувати певні кеш-ключі за допомогою одночасного словника. Таким чином, ви можете запустити GetOrAdd () для двох різних ключів одночасно, але не для одного ключа одночасно.

using System;
using System.Collections.Concurrent;
using System.Web.Caching;

public static class CacheExtensions
{
    private static ConcurrentDictionary<string, object> keyLocks = new ConcurrentDictionary<string, object>();

    /// <summary>
    /// Get or Add the item to the cache using the given key. Lazily executes the value factory only if/when needed
    /// </summary>
    public static T GetOrAdd<T>(this Cache cache, string key, int durationInSeconds, Func<T> factory)
        where T : class
    {
        // Try and get value from the cache
        var value = cache.Get(key);
        if (value == null)
        {
            // If not yet cached, lock the key value and add to cache
            lock (keyLocks.GetOrAdd(key, new object()))
            {
                // Try and get from cache again in case it has been added in the meantime
                value = cache.Get(key);
                if (value == null && (value = factory()) != null)
                {
                    // TODO: Some of these parameters could be added to method signature later if required
                    cache.Insert(
                        key: key,
                        value: value,
                        dependencies: null,
                        absoluteExpiration: DateTime.Now.AddSeconds(durationInSeconds),
                        slidingExpiration: Cache.NoSlidingExpiration,
                        priority: CacheItemPriority.Default,
                        onRemoveCallback: null);
                }

                // Remove temporary key lock
                keyLocks.TryRemove(key, out object locker);
            }
        }

        return value as T;
    }
}

keyLocks.TryRemove(key, out locker)<= яка користь від цього?
iMatoria

2
Це чудово. Вся суть блокування кешу полягає у тому, щоб уникнути дублювання виконаної роботи, щоб отримати значення для цього конкретного ключа. Блокувати весь кеш або навіть його розділи за класом - це нерозумно. Ви хочете саме це - замок із написом "Я отримую значення для <key> всі інші просто чекають мене". Метод розширення теж витончений. Дві чудові ідеї в одній! Це повинна бути відповідь, яку знаходять люди. ДЯКУЮ.
DanO 02.03.18

1
@iMatoria, коли в кеш-пам’яті є щось для цього ключа, немає сенсу зберігати цей об’єкт блокування або запис у словнику ключів - це спробуйте видалити, оскільки блокування, можливо, вже було видалено зі словника іншим Потік, який з’явився першим - усі потоки, які заблокувались, чекаючи на цей ключ, тепер знаходяться в тому розділі коду, де вони просто отримують значення з кешу, але блокування вже немає.
DanO 02.03.18

Мені такий підхід подобається набагато більше, ніж прийнята відповідь. Невелика примітка, однак: спочатку ви використовуєте cache.Key, потім далі використовуєте HttpRuntime.Cache.Get.
staccata

@MindaugasTvaronavicius Хороший улов, ви праві, це крайній випадок, коли Т2 і Т3 виконують factoryметод одночасно. Тільки там, де раніше T1 виконував значення, factoryяке повернуло null (тому значення не кешоване). В іншому випадку T2 і T3 просто отримають кешоване значення одночасно (що повинно бути безпечним). Я думаю, найпростішим рішенням є видалення, keyLocks.TryRemove(key, out locker)але тоді ConcurrentDictionary може стати витоком пам’яті, якщо використовується велика кількість різних ключів. Інакше додайте трохи логіки для підрахунку замків для ключа перед видаленням, можливо, за допомогою семафору?
cwills

13

Просто, щоб повторити те, що сказав Павло, я вважаю, що це найбільш безпечний спосіб написання цього

private T GetOrAddToCache<T>(string cacheKey, GenericObjectParamsDelegate<T> creator, params object[] creatorArgs) where T : class, new()
    {
        T returnValue = HttpContext.Current.Cache[cacheKey] as T;
        if (returnValue == null)
        {
            lock (this)
            {
                returnValue = HttpContext.Current.Cache[cacheKey] as T;
                if (returnValue == null)
                {
                    returnValue = creator(creatorArgs);
                    if (returnValue == null)
                    {
                        throw new Exception("Attempt to cache a null reference");
                    }
                    HttpContext.Current.Cache.Add(
                        cacheKey,
                        returnValue,
                        null,
                        System.Web.Caching.Cache.NoAbsoluteExpiration,
                        System.Web.Caching.Cache.NoSlidingExpiration,
                        CacheItemPriority.Normal,
                        null);
                }
            }
        }

        return returnValue;
    }

7
'lock (this) `- це погано . Ви повинні використовувати спеціальний об'єкт блокування, який не видно для користувачів вашого класу. Припустимо, хтось вирішив використати об’єкт кешу, для блокування. Вони не знатимуть, що він використовується всередині для блокування, що може призвести до поганого стану.
Спендер


2

Я придумав такий спосіб розширення:

private static readonly object _lock = new object();

public static TResult GetOrAdd<TResult>(this Cache cache, string key, Func<TResult> action, int duration = 300) {
    TResult result;
    var data = cache[key]; // Can't cast using as operator as TResult may be an int or bool

    if (data == null) {
        lock (_lock) {
            data = cache[key];

            if (data == null) {
                result = action();

                if (result == null)
                    return result;

                if (duration > 0)
                    cache.Insert(key, result, null, DateTime.UtcNow.AddSeconds(duration), TimeSpan.Zero);
            } else
                result = (TResult)data;
        }
    } else
        result = (TResult)data;

    return result;
}

Я використовував відповіді @John Owen та @ user378380. Моє рішення дозволяє також зберігати значення int та bool у кеші.

Будь ласка, виправте мене, якщо є помилки або це можна написати трохи краще.


Це стандартна довжина кешу 5 мініатюр (60 * 5 = 300 секунд).
nfplee

3
Чудова робота, але я бачу одну проблему: якщо у вас кілька кеш-пам’яті, усі вони матимуть один і той же замок. Щоб зробити його більш надійним, використовуйте словник, щоб отримати замок, відповідний даному кешу.
JoeCool

1

Я бачив нещодавно один зразок, який називали «Правильний шаблон доступу до сумки», який, здавалося, торкався цього.

Я трохи змінив його, щоб бути безпечним для потоків.

http://weblogs.asp.net/craigshoemaker/archive/2008/08/28/asp-net-caching-and-performance.aspx

private static object _listLock = new object();

public List List() {
    string cacheKey = "customers";
    List myList = Cache[cacheKey] as List;
    if(myList == null) {
        lock (_listLock) {
            myList = Cache[cacheKey] as List;
            if (myList == null) {
                myList = DAL.ListCustomers();
                Cache.Insert(cacheKey, mList, null, SiteConfig.CacheDuration, TimeSpan.Zero);
            }
        }
    }
    return myList;
}

Чи не можуть обидва потоки отримати істинний результат для (myList == null)? Потім обидва потоки здійснюють виклик DAL.ListCustomers () і вставляють результати в кеш.
frankadelic

4
Після блокування вам потрібно ще раз перевірити кеш, а не локальну myListзмінну
orip

1
Це було насправді добре перед вашим редагуванням. Блокування не потрібне, якщо ви використовуєте Insertдля запобігання виняткам, лише якщо ви хочете переконатися, що DAL.ListCustomersвикликається один раз (хоча якщо результат нульовий, він буде викликатися кожного разу).
марапет



0

Я змінив код @ user378380 для більшої гнучкості. Замість повернення TResult тепер повертає об'єкт для прийняття різних типів по порядку. Також додано деякі параметри для гнучкості. Вся ідея належить @ user378380.

 private static readonly object _lock = new object();


//If getOnly is true, only get existing cache value, not updating it. If cache value is null then      set it first as running action method. So could return old value or action result value.
//If getOnly is false, update the old value with action result. If cache value is null then      set it first as running action method. So always return action result value.
//With oldValueReturned boolean we can cast returning object(if it is not null) appropriate type on main code.


 public static object GetOrAdd<TResult>(this Cache cache, string key, Func<TResult> action,
    DateTime absoluteExpireTime, TimeSpan slidingExpireTime, bool getOnly, out bool oldValueReturned)
{
    object result;
    var data = cache[key]; 

    if (data == null)
    {
        lock (_lock)
        {
            data = cache[key];

            if (data == null)
            {
                oldValueReturned = false;
                result = action();

                if (result == null)
                {                       
                    return result;
                }

                cache.Insert(key, result, null, absoluteExpireTime, slidingExpireTime);
            }
            else
            {
                if (getOnly)
                {
                    oldValueReturned = true;
                    result = data;
                }
                else
                {
                    oldValueReturned = false;
                    result = action();
                    if (result == null)
                    {                            
                        return result;
                    }

                    cache.Insert(key, result, null, absoluteExpireTime, slidingExpireTime);
                }
            }
        }
    }
    else
    {
        if(getOnly)
        {
            oldValueReturned = true;
            result = data;
        }
        else
        {
            oldValueReturned = false;
            result = action();
            if (result == null)
            {
                return result;
            }

            cache.Insert(key, result, null, absoluteExpireTime, slidingExpireTime);
        }            
    }

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