Hace unas semanas estuvimos echando un vistazo a HybridCache
, la alternativa de .NET 9 al sistema de caché distribuida que apareció con .NET Core años atrás. Como vimos entonces, HybridCache
aporta una serie de ventajas respecto a su predecesor, como la protección contra estampidas, la caché multinivel, invalidación por etiquetas, y otras mejoras.
Sin embargo, también introduce algunos inconvenientes que debemos tener en cuenta si queremos sacarle el máximo partido. En esta ocasión, nos centraremos en la penalización debido a excesiva deserialización de objetos de la caché local, un detalle que puede pasar desapercibido pero que puede tener un impacto significativo en el rendimiento de nuestra aplicación.
¿Cuál es el problema?
Para entenderlo bien, primero vamos a recordar cómo funciona la caché de dos niveles de HybridCache
.
Resumidamente, cuando solicitamos un objeto a HybridCache
, éste lo buscará primero en la caché de primer nivel o caché local; si no está, acudirá a la caché distribuida (segundo nivel) para obtenerlo, y, además de retornarlo al usuario, lo almacenará en la caché local para futuras peticiones.
En la implementación por defecto de HybridCache
, los datos siempre se almacenan en la caché local "envueltos" en objetos de tipo CacheItem<T>
, siendo T
el tipo de datos que estamos manejando. Se trata de una clase interna abstracta que es implementada por dos clases privadas, definidas en el interior de DefaultHybridCache
:
MutableCacheItem<T>
se utiliza cuando el tipo de datos es mutable, como objetos complejos o las que usamos muy habitualmente en nuestras aplicaciones.ImmutableCacheItem<T>
se utiliza cuando el tipo de datos es inmutable, como cadenas de texto, enteros u otros tipos primitivos.
En el caso de ImmutableCacheItem<T>
, el objeto inmutable se almacena en la caché local como una referencia directa al mismo, de forma que los valores solicitados por los clientes se les devuelven directamente, como es habitual cuando, por ejemplo, usamos la clásica caché en memoria IMemoryCache
de .NET.
Sin embargo, en el caso de MutableCacheItem<T>
, lo que se guarda en la caché local es el objeto serializado, que es exactamente la misma secuencia de bytes que viajarán por la red hasta la caché distribuida o de segundo nivel. Por tanto, siempre que se recupera un objeto mutable de la caché local, éste deberá ser deserializado para poder trabajar con él.
Desde el punto de vista del consumidor esto no es un problema, porque los métodos para interactuar con HybridCache
se encargarán de serializar y deserializar los objetos automáticamente y, sea cual sea el caso, siempre retornará al cliente un objeto ya materializado: o bien la referencia almacenada en la caché local, o bien el resultado de deserializar el contenido guardado.
En el caso de los objetos mutables, el hecho de que cada lectura de la caché tenga que deserializar los datos y genere un objeto nuevo es algo positivo, porque cada cliente obtendrá su propio objeto, sin posibilidad de que éste sea alterado por otro proceso. En definitiva, proporciona mayor seguridad en entornos concurrentes.
Sin embargo, estas operaciones de deserialización pueden tener un coste significativo, especialmente en entornos con un gran número de peticiones donde la mayoría de resultados se encuentren en la caché local, ya que se producirá:
- Un mayor consumo de recursos procesamiento (CPU, básicamente).
- Un mayor tiempo de respuesta, ya que la deserialización es una operación intrínsecamente costosa.
- Un mayor uso de memoria, ya que se generará un nuevo objeto cada vez que un cliente acceda al contenido cacheado.
Este consumo extra no debería suponer un problema en la muchos de los casos, pero si trabajamos con objetos complejos o bajo un gran número de peticiones, puede ser un factor a tener en cuenta.
Reutilización de objetos de HybridCache
En función del tipo de datos usado, HybridCache
decidirá si utilizar MutableCacheItem<T>
o ImmutableCacheItem<T>
para almacenarlo en la caché local.
Normalmente, usará ImmutableCacheItem<T>
cuando el tipo de datos sea una cadena de texto o un tipo valor (int
, bool
, DateTime
, etc.), y MutableCacheItem<T>
en el resto de casos. Por ejemplo, la siguiente clase será considerada mutable, por lo que en caché local se almacenará una copia serializada del objeto y, por tanto, cada lectura obligará a realizar una operación de deserialización para materializarlo:
public class CachedFriend
{
public string Name { get; set; }
public int Age { get; set; }
}
Si usamos tipos de datos personalizados que sabemos a ciencia cierta que son inmutables o, aunque no lo sean, si estamos seguros de que nuestro código no va a modificarlos en ningún punto, podemos forzar a que HybridCache
los trate como inmutables, de forma que se reutilice la referencia al mismo objeto en lugar de generar una copia nueva cada vez que se accede a la caché.
Para indicar a HybridCache
que queremos hacer esto, sólo debemos marcar el tipo de datos como sealed
, para indicar que no puede ser extendido, y aplicarle el atributo [ImmutableObject(true)]
para indicar que es inmutable.
El siguiente ejemplo muestra una clase cuyas instancias serían reutilizadas:
[ImmutableObject(true)]
public sealed class CachedFriend
{
public string Name { get; set; }
public int Age { get; set; }
}
Con esta configuración, HybridCache
almacenará las instancias de CachedFriend
en la caché local como referencias directas a memoria, evitando la deserialización y la generación de nuevos objetos cada vez que se accede a la caché y, por tanto, mejorando considerablemente el rendimiento en las lecturas.
¡Espero que os resulte de utilidad!
Publicado en Variable not found.
Aún no hay comentarios, ¡sé el primero!
Enviar un nuevo comentario