Розширюємо і покращуємо Cache в ASP.NET, Різне, Програмування, статті

Про ASP.NET-Об’єкт Cache напевно знає кожен web-розробник на платформі. NET. Зовсім не дивно, адже це єдине рішення для кешування даних web-додатки в ASP.NET, Доступне прямо з коробки.
Досить функціональний і легкий, забезпечений механізмами пріоритету, витіснення, залежностей і зворотних викликів, Cache добре підходить для невеликих додатків, працюючи усередині AppDomain. Здається, Microsoft передбачила все, що необхідно … Але я, тим не менш, хочу зробити його ще трохи краще. Чим же саме?


Синхронізація оновлень



Закешована даними, як і багато чому в нашому світі, з часом властиво втрачати актуальність. Тому, після закінчення відведеного інтервалу часу або при зміні однієї з залежностей, збережений елемент зникне з кешу, і нам доведеться покласти його туди заново. MSDN


розповість

нам, як це зробити, і ми напишемо так:



List<Product> products;
products = (List<Product>)Cache[“Products”];
if (products == null)
{
  products = db.Products.ToList();
  Cache.Insert(“Products”, products);
}

* This source code was highlighted with Source Code Highlighter.



Все виглядає правильно, але рівно до тих пір, поки ми не усвідомлюємо, що код може виконуватися одночасно в декількох потоках. І в цих кількох рядках ми тільки що організували класичне стан гонки (race condition). Нічого страшного, звичайно, не станеться, просто елемент кешу буде оновлено кілька разів, і кожного разу для цього ми звернемося до бази даних. Але це зайва робота, і її можна уникнути, застосувавши звичайну double-check блокування. Ось так:



private static object _lock = new object();


object value;
if ((value = Cache[“Products”]) == null)
{
  lock (_lock)
  {
   if ((value = Cache[“Products”]) == null)
   {
      value = db.Products.ToList();
      Cache.Insert(“Products”, value);
   }
  }
}
var products = (List<Product>)value;

* This source code was highlighted with Source Code Highlighter.



Таким чином, ми гарантуємо, що тільки один потік відправиться в базу даних за списком товарів, а решта почекають його повернення. Можна писати такий код всякий раз, коли ми працюємо з кешем, але краще реалізувати розширення об’єкта Cache за допомогою extension-методу.



Отже,



public static T Get<T>(this Cache cache, string key, object @lock, Func<T> selector,
  DateTime absoluteExpiration)
{
   object value;
   if ((value = cache.Get(key)) == null)
   {
     lock (@lock)
     {
      if ((value = cache.Get(key)) == null)
      {
        value = selector();
        cache.Insert(key, value, null,
         absoluteExpiration, Cache.NoSlidingExpiration,
         CacheItemPriority.Normal, null);
      }
   }
  }
  return (T)value;
}
* This source code was highlighted with Source Code Highlighter.


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


value = Cache.Get(key)

потрібні для того, щоб не отримати таку ж гонку при видаленні елемента кешу в іншому потоці. Тепер для отримання нашого списку товарів ми можемо написати тільки один рядок, а все інше наше розширення візьме на себе. Перевантаження можна додати за смаком 🙂



private static object myLock = new object();

var products = Cache.Get<List<Product>>(“Products”, myLock,
  () => db.Products.ToList(), DateTime.Now.AddMinutes(10));

* This source code was highlighted with Source Code Highlighter.



Отже, з одного завданням ми розправилися, але є ще дещо цікаве. Наприклад, ситуація, коли необхідно оголосити невалідним відразу кілька пов’язаних елементів кешу. ASP.NET Cache надає нам можливість створення залежності від одного або декількох елементів. Приблизно так:



string[] dependencies = { “parent” };
Cache.Insert(“child”, someData,
  new CacheDependency(null, dependencies));

* This source code was highlighted with Source Code Highlighter.



І при оновленні елемента


parent

елемент


child

буде видалено. Поки нічого не нагадує? Що ж, ще трохи коду, і у нас з’явиться повноцінна …



Підтримка тегів і групової інвалідаціі



Підсистема тегування буде працювати цілком прозоро, використовуючи вже описаний механізм залежностей від ключа в кеші. Такими ключами і будуть – вгадайте що? – Теги. При додаванні в кеш елемента з деякою колекцією тегів ми створимо відповідну кількість ключів в кеші і залежність від них.



public static CacheDependency CreateTagDependency(
  this Cache cache, params string[] tags)
{
  if (tags == null // tags.Length < 1)
   return null;

  long version = DateTime.UtcNow.Ticks;
  for (int i = 0; i < tags.Length; ++i)
  {
   cache.Add(“_tag:” + tags[i], version, null,
     DateTime.MaxValue, Cache.NoSlidingExpiration,
     CacheItemPriority.NotRemovable, null);
  }
  return new CacheDependency(null, tags.Select(s =>
   “_tag:” + s).ToArray());
}

* This source code was highlighted with Source Code Highlighter.



Тут як значення версією тега я використовую поточний час, як рекомендувалося в


статті про Memcached

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



Сache.Insert (“key”, value, Сache.CreateTagDependency (“tag1”, “tag2”));

* This source code was highlighted with Source Code Highlighter.



Залишилося зовсім небагато – забезпечити скидання такої групи елементів в кеші. Для цього потрібно всього лише оновити елементи кешу, що представляють цікавлять нас теги. Все інше відбудеться само собою.



public static void Invalidate(this Cache cache, params string[] tags)
{
  long version = DateTime.UtcNow.Ticks;
  for (int i = 0; i < tags.Length; ++i)
  {
   cache.Insert(“_tag:” + tags[i], version, null,
     DateTime.MaxValue, Cache.NoSlidingExpiration,
     CacheItemPriority.NotRemovable, null);
  }
}

* This source code was highlighted with Source Code Highlighter.



Зверніть увагу на те, що в методі, що створює залежність від тегів, використовувався метод


cache.Add

, А тут –


cache.Insert

. Істотна різниця між цими в іншому дуже схожими методами в тому, що перший записує інформацію в кеш тільки в тому випадку, якщо зазначений ключ не був створений раніше, а другий – записує в будь-якому випадку, затираючи старі дані. Ця різниця дуже важливо у нашому випадку, тому що при простому додаванні елемента в кеш нам не потрібно оновлювати вже існуючі теги.



На цьому, здається, і все …



Я вимагаю продовження банкету!



А продовжувати тут ще є куди. Наприклад, можна доопрацювати наведений тут метод Get так, щоб замість негайного видалення дані кеша тимчасово “переїжджали” в іншу комірку, і замість блокування повертати запитану інформацію з неї, поки нові дані завантажуються в кеш.



Можна замість extension-методів зробити якусь абстракцію провайдера кешування і працювати з будь-яким сховищем без зміни коду програми або повністю відключати кеш при налагодженні, використовувати IoC … так мало мало що!



І я сподіваюся, що підходи, описані в моїй статті, виявляться корисними для вас 😉


UPDATE

: Подивившись незамиленим оком на власний код, я побачив в ньому одну негарну річ – блокування при синхронізації виставлялася на весь кеш цілком. Тому я змінив extension-метод


Get

з тим, щоб він приймав користувальницький об’єкт для блокування.

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*