Как кешировать данные в приложении MVC

252

Я прочитал много информации о кэшировании страниц и частичном кэшировании страниц в приложении MVC. Тем не менее, я хотел бы знать, как вы будете кэшировать данные.

В моем сценарии я буду использовать LINQ to Entities (Entity Framework). При первом обращении к GetNames (или какому-либо методу) я хочу получить данные из базы данных. Я хочу сохранить результаты в кеше и при втором вызове использовать кэшированную версию, если она существует.

Может кто-нибудь показать пример того, как это будет работать, где это должно быть реализовано (модель?) И будет ли это работать.

Я видел это в традиционных приложениях ASP.NET, обычно для очень статичных данных.

Coolcoder
источник
1
Рассматривая ответы ниже, обязательно подумайте, хотите ли вы, чтобы ваш контроллер знал / отвечал за доступ к данным и вопросы кэширования. Как правило, вы хотите отделить это. См. Шаблон репозитория для хорошего способа сделать это: deviq.com/repository-pattern
ssmith

Ответы:

75

Ссылайтесь на dll System.Web в вашей модели и используйте System.Web.Caching.Cache

    public string[] GetNames()
    {
      string[] names = Cache["names"] as string[];
      if(names == null) //not in cache
      {
        names = DB.GetNames();
        Cache["names"] = names;
      }
      return names;
    }

Немного упрощенно, но я думаю, это сработает. Это не специфично для MVC, и я всегда использовал этот метод для кэширования данных.

terjetyl
источник
89
Я не рекомендую это решение: в ответ вы можете снова получить нулевой объект, потому что он перечитывает данные в кеше и, возможно, он уже был удален из кеша. Я бы лучше сделал: public string [] GetNames () {string [] noms = Cache ["names"]; if (noms == null) {noms = DB.GetNames (); Cache ["names"] = noms; } return (noms); }
Оли
Я согласен с Оли ... получить результаты от реального вызова в БД лучше, чем получить их из кэша
CodeClimber
1
Работает ли это с DB.GetNames().AsQueryableметодом задержки запроса?
Чейз Флорелл
Нет, если вы не измените возвращаемое значение со строки [] на IEnumerable <string>
terjetyl
12
Если вы не установите срок действия ... когда срок действия кэша истекает по умолчанию?
Чака
403

Вот хороший и простой кеш-класс / сервис, который я использую:

using System.Runtime.Caching;  

public class InMemoryCache: ICacheService
{
    public T GetOrSet<T>(string cacheKey, Func<T> getItemCallback) where T : class
    {
        T item = MemoryCache.Default.Get(cacheKey) as T;
        if (item == null)
        {
            item = getItemCallback();
            MemoryCache.Default.Add(cacheKey, item, DateTime.Now.AddMinutes(10));
        }
        return item;
    }
}

interface ICacheService
{
    T GetOrSet<T>(string cacheKey, Func<T> getItemCallback) where T : class;
}

Использование:

cacheProvider.GetOrSet("cache key", (delegate method if cache is empty));

Поставщик кэша проверит, есть ли что-нибудь под именем «id кэша» в кэше, а если нет, то вызовет метод делегата для извлечения данных и сохранения их в кэше.

Пример:

var products=cacheService.GetOrSet("catalog.products", ()=>productRepository.GetAll())
Hrvoje Hudo
источник
3
Я адаптировал это так, чтобы механизм кэширования использовался для каждой пользовательской сессии, используя вместо этого HttpContext.Current.Session. Я также поместил свойство Cache в свой класс BaseController, так что его легкий доступ и обновленный конструктор позволяют использовать DI для модульного тестирования. Надеюсь это поможет.
WestDiscGolf
1
Вы также можете сделать этот класс и метод статическими для повторного использования среди других контроллеров.
Алекс
5
Этот класс не должен зависеть от HttpContext. Я упростил это только для примера. Объект кэша должен быть вставлен через конструктор - его можно заменить другими механизмами кэширования. Все это достигается с помощью IoC / DI, а также статического (одноэлементного) жизненного цикла.
Hrvoje Hudo
3
@Brendan - и что еще хуже, он имеет магические строки для ключей кэша, а не выводит их из имени метода и параметров.
Ссмит
5
Это потрясающее решение низкого уровня. Как уже упоминали другие, вы захотите обернуть это в безопасный для конкретного класса класс. Доступ к этому непосредственно в ваших контроллерах был бы кошмаром обслуживания из-за волшебных последовательностей.
Джош Ноу
43

Я имею в виду пост ТТ и предлагаю следующий подход:

Ссылайтесь на dll System.Web в вашей модели и используйте System.Web.Caching.Cache

public string[] GetNames()
{ 
    var noms = Cache["names"];
    if(noms == null) 
    {    
        noms = DB.GetNames();
        Cache["names"] = noms; 
    }

    return ((string[])noms);
}

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

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

Oli
источник
Но разве строка не Cache["names"] = noms;помещается в кеш?
Омар
2
@ Бадди Да, это так. Но этот пример отличается от первого, на который ссылается Оли, потому что он не обращается к кешу снова - проблема в том, что он просто делает: return (string []) Cache ["names"]; .. МОЖЕТ привести к возвращению нулевого значения, поскольку оно МОЖЕТ истечь. Это маловероятно, но это может случиться. Этот пример лучше, потому что мы сохраняем фактическое значение, возвращаемое из базы данных в памяти, кешируем это значение, а затем возвращаем это значение, а не значение, повторно считанное из кэша.
jamiebarrow
Или ... значение, перечитанное из кэша, если оно все еще существует (! = Null). Отсюда и весь смысл кеширования. Это просто говорит о том, что он дважды проверяет нулевые значения и читает базу данных, где это необходимо. Очень умно, спасибо Оли!
Шон Кендл
Не могли бы вы поделиться ссылкой, где я могу прочитать о кэшировании приложений на основе Key Value. Я не могу найти ссылки.
Unbreakable
@Oli, Как использовать записи кэша из CSHTML или HTML-страницы
Дипан Радж
37

Для .NET 4.5+ Framework

добавить ссылку: System.Runtime.Caching

добавить использование оператора: using System.Runtime.Caching;

public string[] GetNames()
{ 
    var noms = System.Runtime.Caching.MemoryCache.Default["names"];
    if(noms == null) 
    {    
        noms = DB.GetNames();
        System.Runtime.Caching.MemoryCache.Default["names"] = noms; 
    }

    return ((string[])noms);
}

В .NET Framework 3.5 и более ранних версиях ASP.NET предоставляла реализацию кэширования в памяти в пространстве имен System.Web.Caching. В предыдущих версиях .NET Framework кэширование было доступно только в пространстве имен System.Web и поэтому требовало зависимости от классов ASP.NET. В .NET Framework 4 пространство имен System.Runtime.Caching содержит API-интерфейсы, разработанные как для веб-приложений, так и для не-веб-приложений.

Больше информации:

ЮФО
источник
Откуда Db.GerNames()идет?
младший
DB.GetNames - это просто метод из DAL, который выбирает некоторые имена из базы данных. Это то, что вы обычно получаете.
ЮФО
Это должно быть на вершине, поскольку у него есть актуальное актуальное решение
BYISHIMO Audace
2
Спасибо, нужно было добавить пакет nuget System.Runtime.Caching (v4.5).
Стив Грин
26

Стив Смит сделал два отличных поста в блоге, которые демонстрируют, как использовать его шаблон CachedRepository в ASP.NET MVC. Он эффективно использует шаблон репозитория и позволяет вам получать кэширование без необходимости изменять существующий код.

http://ardalis.com/Introducing-the-CachedRepository-Pattern

http://ardalis.com/building-a-cachedrepository-via-strategy-pattern

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

Брендан Энрик
источник
1
Великие посты! Спасибо, что поделился!!
Марк Гуд
Ссылки не работают по состоянию на 2013-08-31.
CBono
Не могли бы вы поделиться ссылкой, где я могу прочитать о кэшировании приложений на основе Key Value. Я не могу найти ссылки.
Unbreakable
4

Кэширование AppFabric - это распределенная технология кэширования в памяти, которая хранит данные в парах ключ-значение с использованием физической памяти на нескольких серверах. AppFabric обеспечивает улучшения производительности и масштабируемости для приложений .NET Framework. Концепции и архитектура

Арун Дут
источник
Это относится только к Azure, а не к ASP.NET MVC в целом.
Генри К
3

Расширяя ответ @Hrvoje Hudo ...

Код:

using System;
using System.Runtime.Caching;

public class InMemoryCache : ICacheService
{
    public TValue Get<TValue>(string cacheKey, int durationInMinutes, Func<TValue> getItemCallback) where TValue : class
    {
        TValue item = MemoryCache.Default.Get(cacheKey) as TValue;
        if (item == null)
        {
            item = getItemCallback();
            MemoryCache.Default.Add(cacheKey, item, DateTime.Now.AddMinutes(durationInMinutes));
        }
        return item;
    }

    public TValue Get<TValue, TId>(string cacheKeyFormat, TId id, int durationInMinutes, Func<TId, TValue> getItemCallback) where TValue : class
    {
        string cacheKey = string.Format(cacheKeyFormat, id);
        TValue item = MemoryCache.Default.Get(cacheKey) as TValue;
        if (item == null)
        {
            item = getItemCallback(id);
            MemoryCache.Default.Add(cacheKey, item, DateTime.Now.AddMinutes(durationInMinutes));
        }
        return item;
    }
}

interface ICacheService
{
    TValue Get<TValue>(string cacheKey, Func<TValue> getItemCallback) where TValue : class;
    TValue Get<TValue, TId>(string cacheKeyFormat, TId id, Func<TId, TValue> getItemCallback) where TValue : class;
}

Примеры

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

Product product = cache.Get("product_{0}", productId, 10, productData.getProductById);

Кеширование всего чего-либо

IEnumerable<Categories> categories = cache.Get("categories", 20, categoryData.getCategories);

Почему

Второй помощник особенно хорош, потому что большинство ключей данных не являются составными. Дополнительные методы могут быть добавлены, если вы часто используете составные ключи. Таким образом, вы избегаете всякой конкатенации строк или форматов строк. Чтобы получить ключ для передачи помощнику кеша. Это также облегчает передачу метода доступа к данным, потому что вам не нужно передавать идентификатор в метод-оболочку ... все становится очень кратким и последовательным для большинства случаев использования.

smdrager
источник
1
В определениях вашего интерфейса отсутствует параметр "durationInMinutes". ;-)
Tech0
3

Вот улучшение ответа Хрвое Худо. Эта реализация имеет несколько ключевых улучшений:

  • Ключи кеша создаются автоматически на основе функции обновления данных и передаваемого объекта, который определяет зависимости
  • Передать промежуток времени для любой продолжительности кэша
  • Использует замок для безопасности потока

Обратите внимание, что это зависит от Newtonsoft.Json, чтобы сериализовать объект depenOn, но это может быть легко заменено для любого другого метода сериализации.

ICache.cs

public interface ICache
{
    T GetOrSet<T>(Func<T> getItemCallback, object dependsOn, TimeSpan duration) where T : class;
}

InMemoryCache.cs

using System;
using System.Reflection;
using System.Runtime.Caching;
using Newtonsoft.Json;

public class InMemoryCache : ICache
{
    private static readonly object CacheLockObject = new object();

    public T GetOrSet<T>(Func<T> getItemCallback, object dependsOn, TimeSpan duration) where T : class
    {
        string cacheKey = GetCacheKey(getItemCallback, dependsOn);
        T item = MemoryCache.Default.Get(cacheKey) as T;
        if (item == null)
        {
            lock (CacheLockObject)
            {
                item = getItemCallback();
                MemoryCache.Default.Add(cacheKey, item, DateTime.Now.Add(duration));
            }
        }
        return item;
    }

    private string GetCacheKey<T>(Func<T> itemCallback, object dependsOn) where T: class
    {
        var serializedDependants = JsonConvert.SerializeObject(dependsOn);
        var methodType = itemCallback.GetType();
        return methodType.FullName + serializedDependants;
    }
}

Использование:

var order = _cache.GetOrSet(
    () => _session.Set<Order>().SingleOrDefault(o => o.Id == orderId)
    , new { id = orderId }
    , new TimeSpan(0, 10, 0)
);
DShook
источник
2
if (item == null)Должен быть внутри замка. Теперь, когда это ifдо блокировки, может возникнуть состояние гонки. Или, что еще лучше, вы должны сохранить ifперед блокировкой, но перепроверить, если кэш все еще пуст в качестве первой строки внутри блокировки. Потому что, если два потока приходят одновременно, они оба обновляют кеш. Ваша текущая блокировка не помогает.
Аль Кепп
3
public sealed class CacheManager
{
    private static volatile CacheManager instance;
    private static object syncRoot = new Object();
    private ObjectCache cache = null;
    private CacheItemPolicy defaultCacheItemPolicy = null;

    private CacheEntryRemovedCallback callback = null;
    private bool allowCache = true;

    private CacheManager()
    {
        cache = MemoryCache.Default;
        callback = new CacheEntryRemovedCallback(this.CachedItemRemovedCallback);

        defaultCacheItemPolicy = new CacheItemPolicy();
        defaultCacheItemPolicy.AbsoluteExpiration = DateTime.Now.AddHours(1.0);
        defaultCacheItemPolicy.RemovedCallback = callback;
        allowCache = StringUtils.Str2Bool(ConfigurationManager.AppSettings["AllowCache"]); ;
    }
    public static CacheManager Instance
    {
        get
        {
            if (instance == null)
            {
                lock (syncRoot)
                {
                    if (instance == null)
                    {
                        instance = new CacheManager();
                    }
                }
            }

            return instance;
        }
    }

    public IEnumerable GetCache(String Key)
    {
        if (Key == null || !allowCache)
        {
            return null;
        }

        try
        {
            String Key_ = Key;
            if (cache.Contains(Key_))
            {
                return (IEnumerable)cache.Get(Key_);
            }
            else
            {
                return null;
            }
        }
        catch (Exception)
        {
            return null;
        }
    }

    public void ClearCache(string key)
    {
        AddCache(key, null);
    }

    public bool AddCache(String Key, IEnumerable data, CacheItemPolicy cacheItemPolicy = null)
    {
        if (!allowCache) return true;
        try
        {
            if (Key == null)
            {
                return false;
            }

            if (cacheItemPolicy == null)
            {
                cacheItemPolicy = defaultCacheItemPolicy;
            }

            String Key_ = Key;

            lock (Key_)
            {
                return cache.Add(Key_, data, cacheItemPolicy);
            }
        }
        catch (Exception)
        {
            return false;
        }
    }

    private void CachedItemRemovedCallback(CacheEntryRemovedArguments arguments)
    {
        String strLog = String.Concat("Reason: ", arguments.RemovedReason.ToString(), " | Key-Name: ", arguments.CacheItem.Key, " | Value-Object: ", arguments.CacheItem.Value.ToString());
        LogManager.Instance.Info(strLog);
    }
}
Чау
источник
3
Попробуйте добавить какое-нибудь объяснение
Майк Дебела
2

Я использовал это таким образом, и это работает для меня. https://msdn.microsoft.com/en-us/library/system.web.caching.cache.add(v=vs.110).aspx информация о параметрах для system.web.caching.cache.add.

public string GetInfo()
{
     string name = string.Empty;
     if(System.Web.HttpContext.Current.Cache["KeyName"] == null)
     {
         name = GetNameMethod();
         System.Web.HttpContext.Current.Cache.Add("KeyName", name, null, DateTime.Noew.AddMinutes(5), Cache.NoSlidingExpiration, CacheitemPriority.AboveNormal, null);
     }
     else
     {
         name = System.Web.HttpContext.Current.Cache["KeyName"] as string;
     }

      return name;

}
user3776645
источник
дополнительные голоса для полностью подходящего материала с полным пространством имен !!
Ninjanoel
1

Я использую два класса. Первый объект ядра кеша:

public class Cacher<TValue>
    where TValue : class
{
    #region Properties
    private Func<TValue> _init;
    public string Key { get; private set; }
    public TValue Value
    {
        get
        {
            var item = HttpRuntime.Cache.Get(Key) as TValue;
            if (item == null)
            {
                item = _init();
                HttpContext.Current.Cache.Insert(Key, item);
            }
            return item;
        }
    }
    #endregion

    #region Constructor
    public Cacher(string key, Func<TValue> init)
    {
        Key = key;
        _init = init;
    }
    #endregion

    #region Methods
    public void Refresh()
    {
        HttpRuntime.Cache.Remove(Key);
    }
    #endregion
}

Второй - список объектов кеша:

public static class Caches
{
    static Caches()
    {
        Languages = new Cacher<IEnumerable<Language>>("Languages", () =>
                                                          {
                                                              using (var context = new WordsContext())
                                                              {
                                                                  return context.Languages.ToList();
                                                              }
                                                          });
    }
    public static Cacher<IEnumerable<Language>> Languages { get; private set; }
}
Berezh
источник
0

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

 public class GPDataDictionary
{
    private Dictionary<string, object> configDictionary = new Dictionary<string, object>();

    /// <summary>
    /// Configuration values dictionary
    /// </summary>
    public Dictionary<string, object> ConfigDictionary
    {
        get { return configDictionary; }
    }

    private static GPDataDictionary instance;
    public static GPDataDictionary Instance
    {
        get
        {
            if (instance == null)
            {
                instance = new GPDataDictionary();
            }
            return instance;
        }
    }

    // private constructor
    private GPDataDictionary() { }

}  // singleton
GeraGamo
источник
Это отлично сработало для меня, поэтому я рекомендую это всем, кому это может помочь
GeraGamo
0
HttpContext.Current.Cache.Insert("subjectlist", subjectlist);
Ахтар Уззаман
источник
-8

Вы также можете попробовать использовать кэширование, встроенное в ASP MVC:

Добавьте следующий атрибут в метод контроллера, который вы хотите кэшировать:

[OutputCache(Duration=10)]

В этом случае ActionResult этого будет кэшироваться в течение 10 секунд.

Подробнее об этом здесь

Квай
источник
4
OutputCache предназначен для рендеринга Action, вопрос касался кеширования данных, а не страницы.
Coolcoder
это не по теме, но OutputCache также
кэширует