Cache – czyli jak uniknąć problemów wydajnościowych serwisów?

Ciężko wyobrazić sobie program bez bazy danych. W tym artykule na prostym przykładzie pokażę Ci jak wprowadzić mechanizm cachowania w aplikacji wykorzystującej technologię ASP .Net MVC.

Zacznijmy od definicji zaczerpniętej wprost z Wikipedii:

Pamięć podręczna (ang. cache) – mechanizm, w którym część spośród danych zgromadzonych w źródłach o długim czasie dostępu i niższej przepustowości jest dodatkowo przechowywana w pamięci o lepszych parametrach. Ma to na celu poprawę szybkości dostępu do tych informacji, które przypuszczalnie będą potrzebne w najbliższej przyszłości.

W praktyce cachowanie to zapisanie pewnego zbioru danych do pamięci podręcznej na określą jednostkę czasu, by zmniejszyć koszt zużycia zasobów serwera. Dzięki temu użytkownik aplikacji będzie miał możliwie najszybszy dostęp do interesującej go treści.

Przykładem którym się posłużymy będzie sklep internetowy (jak wspomniałem wcześniej wykorzystujący technologię ASP .Net MVC). W bazie danych przechowujemy wszelkie potrzebne informacje do funkcjonowania serwisu – będą to zarówno produkty jak i ich kategorie.

Sklep korzysta z dynamicznych widoków HTML z automatyczną generacją produktów czy kategorii z bazy danych. Już na pierwszy rzut oka widzimy, że zmienność części przekazywanych informacji jest niewielka. Zastosowanie mechanizmów cachowania będzie tu dla nas idealnym rozwiązaniem.

Cachowanie w praktyce

Możliwości jest wiele, opowiem Ci o dwóch.

1. Wykorzystanie atrybutu OutputCache będącego filtrem akcji kontrolerów.

Aby tego dokonać umieszczamy wyżej wymieniony atrybut przed interesującą nas akcją w kontrolerze.

[ChildActionOnly]
[OutputCache(Duration = 80000)]
public ActionResult CategoriesMenu()
{
    var categories = db.Categories.ToList();

    return PartialView("_CategoriesMenu",categories);
}

Atrybut OutputCache posiada szereg parametrów których konfiguracją możemy zająć się osobiście. W powyższym przykładzie ustawiono jedynie Duration na 80 000 (sekund), domyślna wartość parametru Location to Any.

Zostańmy jeszcze chwilę dłużej przy Location, aby ją ustawić – czyli miejsce gdzie dane mają być cachowane napiszemy:

[OutputCache(Location=OutputCacheLocation.Any)]
public ActionResult Index()
{
    return View();
}

Dostępne są następujące opcje mówiące nam o tym gdzie znajdzie się pamięć podręczna:

  • Any – może znajdować się na kliencie przeglądarki, serwerze proxy lub na serwerze na którym przetworzono żądanie (defaultowo wybierana)
  • Client – znajduje się na kliencie przeglądarki z którego pochodzi żądanie
  • Server znajduje się na serwerze sieci Web na którym przetworzono żądanie
  • Server and Client – po stronie serwera lub klienta
  • Downstream – gdzie indziej niż serwer aplikacji
  • None – brak cachowania

Informację o pozostałych parametrach znajdziesz opisane w dokumentacji MSDN.

Jak widzimy OutputCache to bardzo prosty mechanizm który z łatwością możemy dodać do wybranych akcji.

2. DataCaching – pozwalający na ręczne dodanie obiektów do Cache.

 

Domyślny mechanizm to tzw ASP .Net Caching. Może się jednak zdarzyć, że kiedyś postanowimy go zastąpić innym np. Redis, Memcached, OrmLiteCacheClient. Aby mieć na zapas możliwe przepięcie się na inny provider cachujący skorzystamy z dobrodziejstw jakie niosą ze sobą interfejsy.

namespace BoardGameShopMVC.Infrastructure.CacheProvider
{
    public interface ICacheProvider
    {
        object Get(string key);
        void Set(string key, object data, int cacheTime);
        bool IsSet(string key);
        void Invalidate(string key);
    }
}

Zaimplementujemy go:

using System;
using System.Web.Caching;
using System.Web;

namespace BoardGameShopMVC.Infrastructure.CacheProvider
{
    public class DefaultCacheProvider : ICacheProvider
    {
        private Cache Cache { get { return HttpContext.Current.Cache; } }

        public object Get(string key)
        {
            return Cache[key];
        }

        public void Set(string key, object data, int cacheTime)
        {
            var expirationTime = DateTime.Now + TimeSpan.FromMinutes(cacheTime);
            Cache.Insert(key, data, null, expirationTime, Cache.NoSlidingExpiration);
        }

        public bool IsSet(string key)
        {
            return (Cache[key] != null);
        }

        public void Invalidate(string key)
        {
            Cache.Remove(key);
        }
    }
}

Aby uniknąć pomyłek tworzymy pomocniczą klasę przetrzymującą const string

namespace BoardGameShopMVC.Infrastructure.CacheProvider
{
    public class Consts
    { 
        public const string NewItemsCacheKey = "NewItemsCacheKey";
    }
}

Ostateczne wykorzystanie powyższych klas w konkretnym kontrolerze i akcji wygląda następująco:

public class HomeController : Controller
{
    private StoreContext db = new StoreContext();

    public ActionResult Index()
    {
        var categories = db.Categories.ToList();

        //Caching 
        ICacheProvider cache = new DefaultCacheProvider();
        List<Game> newArriwals;

        if (cache.IsSet(Consts.NewItemsCacheKey))
        {
             newArriwals = cache.Get(Consts.NewItemsCacheKey) as List<Game>;
        }
        else
        {
           newArriwals = db.Games.Where(x => !x.IsHidder).OrderByDescending(x => x.DateAdded).Take(3).ToList();
           cache.Set(Consts.NewItemsCacheKey, newArriwals, 30);
        }
            
        var bestsellers = db.Games.Where(x => !x.IsHidder && x.IsBestseller).OrderBy(y => Guid.NewGuid()).Take(3).ToList(); // order by new Guid to get random bestseller in each time

        var vm = new HomeViewModel()
        {
            Bestsellers = bestsellers,
            NewArrivals = newArriwals,
            Categories = categories
        };
        return View(vm);
    }
}

Teraz już wiesz w jaki sposób, możesz poprawiać wydajność swoich aplikacji. Mechanizm cachowania jest powszechnie wykorzystywany, znany i lubiany.

Pozdrawiam Wojtek