Cómo almacenar en caché los datos en una aplicación MVC

252

He leído mucha información sobre el almacenamiento en caché de páginas y el almacenamiento en caché parcial de páginas en una aplicación MVC. Sin embargo, me gustaría saber cómo almacenaría en caché los datos.

En mi escenario, usaré LINQ to Entities (entidad framework). En la primera llamada a GetNames (o cualquiera que sea el método), quiero obtener los datos de la base de datos. Quiero guardar los resultados en caché y en la segunda llamada para usar la versión en caché si existe.

¿Alguien puede mostrar un ejemplo de cómo funcionaría esto, dónde debería implementarse (modelo?) Y si funcionaría.

He visto esto hecho en aplicaciones ASP.NET tradicionales, típicamente para datos muy estáticos.

Coolcoder
fuente
1
Al revisar las respuestas a continuación, asegúrese de considerar si desea que su controlador tenga conocimiento / responsabilidad sobre el acceso a los datos y los problemas de almacenamiento en caché. Generalmente quieres separar esto. Vea el Patrón de repositorio para una buena manera de hacerlo: deviq.com/repository-pattern
ssmith

Respuestas:

75

Consulte el archivo System.Web dll en su modelo y use 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;
    }

Un poco simplificado pero supongo que funcionaría. Esto no es específico de MVC y siempre he usado este método para almacenar datos en caché.

terjetyl
fuente
89
No recomiendo esta solución: en la devolución, es posible que obtenga un objeto nulo nuevamente, porque se está volviendo a leer en el caché y es posible que ya se haya eliminado del caché. Prefiero hacer: cadena pública [] GetNames () {cadena [] noms = Caché ["nombres"]; if (noms == null) {noms = DB.GetNames (); Caché ["nombres"] = noms; } return (noms); }
Oli el
Estoy de acuerdo con Oli ... obtener los resultados de la llamada real a la base de datos es mejor que obtenerlos de la caché
CodeClimber
1
¿Funciona esto con el DB.GetNames().AsQueryablemétodo de retrasar la consulta?
Chase Florell
No, a menos que cambie el valor de retorno de la cadena [] a IEnumerable <cadena>
terjetyl
12
Si no establece la caducidad ... ¿cuándo caduca la memoria caché de forma predeterminada?
Chaka
403

Aquí hay una clase / servicio de ayuda de caché simple y agradable que uso:

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

Uso:

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

El proveedor de caché verificará si hay algo con el nombre de "ID de caché" en el caché, y si no lo hay, llamará a un método delegado para obtener datos y almacenarlos en caché.

Ejemplo:

var products=cacheService.GetOrSet("catalog.products", ()=>productRepository.GetAll())
Hrvoje Hudo
fuente
3
He adaptado esto para que el mecanismo de almacenamiento en caché se use por sesión de usuario utilizando HttpContext.Current.Session en su lugar. También puse una propiedad Cache en mi clase BaseController para que su fácil acceso y el constructor actualizado permitan la DI para pruebas unitarias. Espero que esto ayude.
WestDiscGolf
1
También puede hacer que esta clase y método sea estático para su reutilización entre otros controladores.
Alex
55
Esta clase no debería depender de HttpContext. Lo simplifiqué solo por ejemplo aquí. El objeto de caché se debe insertar a través del constructor; se puede reemplazar luego con otros mecanismos de caché. Todo esto se logra con IoC / DI, junto con el ciclo de vida estático (singleton).
Hrvoje Hudo
3
@Brendan, y peor aún, tiene cadenas mágicas para las claves de caché, en lugar de inferirlas del nombre del método y los parámetros.
ssmith
55
Esta es una increíble solución de bajo nivel. Al igual que otros han aludido, desearía envolver esto en una clase segura de tipo específica de dominio. Acceder a esto directamente en sus controladores sería una pesadilla de mantenimiento debido a las cadenas mágicas.
Josh Noe
43

Me refiero a la publicación de TT y sugiero el siguiente enfoque:

Consulte el archivo System.Web dll en su modelo y use System.Web.Caching.Cache

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

    return ((string[])noms);
}

No debe devolver un valor de relectura del caché, ya que nunca sabrá si en ese momento específico todavía está en el caché. Incluso si lo insertó en la declaración anterior, es posible que ya haya desaparecido o que nunca se haya agregado al caché, simplemente no lo sabe.

Entonces agrega los datos leídos de la base de datos y los devuelve directamente, no releyendo de la caché.

Oli
fuente
¿Pero no se pone la línea Cache["names"] = noms;en el caché?
Omar
2
@Baddie Sí, lo hace. Pero este ejemplo es diferente al primero al que se refiere Oli, porque no vuelve a acceder al caché; el problema es que simplemente lo hace: return (string []) Cache ["names"]; .. PODRÍA resultar en un valor nulo devuelto, porque PODRÍA haber expirado. No es probable, pero puede suceder. Este ejemplo es mejor, porque almacenamos el valor real devuelto por la base de datos en la memoria, almacenamos en caché ese valor y luego devolvemos ese valor, no el valor que se vuelve a leer de la memoria caché.
jamiebarrow
O ... el valor relectura del caché, si aún existe (! = Nulo). Por lo tanto, todo el punto de almacenamiento en caché. Esto es solo para decir que verifica dos veces los valores nulos y lee la base de datos cuando es necesario. Muy inteligente, gracias Oli!
Sean Kendle
¿Puede compartir algún enlace donde pueda leer sobre el almacenamiento en caché de aplicaciones basado en Key Value? No puedo encontrar enlaces.
Irrompible el
@Oli, Cómo consumir estos registros de caché desde la página CSHTML o HTML
Deepan Raj
37

Para .NET 4.5+ framework

añadir referencia: System.Runtime.Caching

agregar usando la declaración: 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);
}

En .NET Framework 3.5 y versiones anteriores, ASP.NET proporcionó una implementación de memoria caché en memoria en el espacio de nombres System.Web.Caching. En versiones anteriores de .NET Framework, el almacenamiento en caché solo estaba disponible en el espacio de nombres System.Web y, por lo tanto, requería una dependencia de las clases ASP.NET. En .NET Framework 4, el espacio de nombres System.Runtime.Caching contiene API diseñadas para aplicaciones web y no web.

Más información:

juFo
fuente
De donde Db.GerNames()viene
Junior
DB.GetNames es solo un método del DAL que obtiene algunos nombres de la base de datos. Esto es lo que normalmente recuperarías.
juFo
Esto debería estar en la cima ya que tiene la solución relevante actual
BYISHIMO Audace
2
Gracias, necesitaba agregar el paquete Nuget System.Runtime.Caching también (v4.5).
Steve Greene
26

Steve Smith hizo dos excelentes publicaciones en el blog que demuestran cómo usar su patrón CachedRepository en ASP.NET MVC. Utiliza el patrón de repositorio de manera efectiva y le permite obtener el almacenamiento en caché sin tener que cambiar su código existente.

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

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

En estas dos publicaciones, muestra cómo configurar este patrón y también explica por qué es útil. Al usar este patrón, obtiene el almacenamiento en caché sin que su código existente vea ninguna de las lógicas de almacenamiento en caché. Esencialmente, utiliza el repositorio en caché como si fuera cualquier otro repositorio.

Brendan Enrick
fuente
1
Grandes publicaciones! ¡¡Gracias por compartir!!
Mark Good
Enlaces muertos a partir del 2013-08-31.
CBono
¿Puede compartir algún enlace donde pueda leer sobre el almacenamiento en caché de aplicaciones basado en Key Value? No puedo encontrar enlaces.
Irrompible el
4

AppFabric Caching se distribuye y es una técnica de almacenamiento en caché en memoria que almacena datos en pares clave-valor utilizando memoria física en varios servidores. AppFabric proporciona mejoras de rendimiento y escalabilidad para aplicaciones .NET Framework. Conceptos y arquitectura

Arun Duth
fuente
Esto es específico de Azure, no de ASP.NET MVC en general.
Henry C
3

Ampliando la respuesta de @Hrvoje Hudo ...

Código:

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

Ejemplos

Almacenamiento en caché de un solo elemento (cuando cada elemento se almacena en caché en función de su ID porque el almacenamiento en caché de todo el catálogo para el tipo de artículo sería demasiado intenso).

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

Guardar en caché todo

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

Por qué TId

El segundo ayudante es especialmente bueno porque la mayoría de las claves de datos no son compuestas. Se podrían agregar métodos adicionales si utiliza claves compuestas a menudo. De esta forma, evita hacer todo tipo de concatenación de cadenas o cadenas. Formatos para que la clave pase al ayudante de caché. También facilita la transferencia del método de acceso a datos porque no tiene que pasar la ID al método de envoltura ... todo se vuelve muy conciso y consistente para la mayoría de los casos de uso.

smdrager
fuente
1
Las definiciones de su interfaz no tienen el parámetro "DurationInMinutes". ;-)
Tech0
3

Aquí hay una mejora en la respuesta de Hrvoje Hudo. Esta implementación tiene un par de mejoras clave:

  • Las claves de caché se crean automáticamente en función de la función para actualizar los datos y el objeto pasado que especifica las dependencias
  • Pase en el intervalo de tiempo para cualquier duración de caché
  • Utiliza un candado para la seguridad del hilo

Tenga en cuenta que esto depende de Newtonsoft.Json para serializar el objeto dependOn, pero que puede cambiarse fácilmente por cualquier otro método de serialización.

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

Uso:

var order = _cache.GetOrSet(
    () => _session.Set<Order>().SingleOrDefault(o => o.Id == orderId)
    , new { id = orderId }
    , new TimeSpan(0, 10, 0)
);
DShook
fuente
2
El if (item == null)debe estar dentro de la cerradura. Ahora, cuando esto ifestá antes del bloqueo, puede ocurrir una condición de carrera. O incluso mejor, debe mantenerlo ifantes del bloqueo, pero vuelva a verificar si el caché aún está vacío como la primera línea dentro del bloqueo. Porque si dos hilos llegan al mismo tiempo, ambos actualizan el caché. Su bloqueo actual no es útil.
Al Kepp
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);
    }
}
Chau
fuente
3
Considere agregar alguna explicación
Mike Debela
2

Lo he usado de esta manera y funciona para mí. https://msdn.microsoft.com/en-us/library/system.web.caching.cache.add(v=vs.110).aspx información de parámetros para 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;

}
usuario3776645
fuente
¡Votaciones adicionales para cosas que califican completamente con su espacio de nombres completo!
Ninjanoel
1

Yo uso dos clases. Primero, el objeto central de caché:

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
}

El segundo es la lista de objetos de caché:

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
fuente
0

Diré que implementar Singleton en este problema persistente de datos puede ser una solución para este asunto en caso de que encuentre soluciones anteriores mucho más complicadas

 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
fuente
Esto funcionó perfectamente para mí, es por eso que recomiendo esto a todos los que puedan ser ayudados por esto
GeraGamo
0
HttpContext.Current.Cache.Insert("subjectlist", subjectlist);
Md. Akhtar Uzzaman
fuente
-8

También puede probar y usar el almacenamiento en caché integrado en ASP MVC:

Agregue el siguiente atributo al método de controlador que desea almacenar en caché:

[OutputCache(Duration=10)]

En este caso, el ActionResult de esto se almacenará en caché durante 10 segundos.

Más sobre esto aquí

qui
fuente
44
OutputCache es para la representación de Action, la pregunta estaba relacionada con el almacenamiento en caché de datos, no con la página.
Coolcoder
está fuera de tema pero OutputCache también almacena en caché los datos de la base de datos
Muflix