Utilizando Redis como sistema de cache en Symfony

En este artículo voy a incluir una implementación de como utilizar la tecnología Redis como sistema de cache en el framework Symfony. En uno de los proyectos que trabajé, el equipo de desarrollo decidió utilizar esta tecnología como sistema de cache dentro de nuestra aplicación web, mejorando el performance de varias partes del sistema. Mostraré un breve repaso sobre “que es Redis”, “quien lo está utilizando”, el “caso de uso” en en el que emplee esta tecnología y una implementación con el framework Symfony.

¿Que es Redis?

Redis es un motor de base de datos basado en almacenamiento clave/valor. Este motor trabaja muy rápido al trabajar la información en memoria y ofrece la posibilidad de almacenar la información en estructuras de datos diversas como listas, sets o hashes. Es uno de las base de datos nosql basada en clave/valor mas popular y es altamente empleada en aplicaciones web. Esto se debe en un alto porcentaje al performance que ofrece, y también a la sencillez de uso y flexibilidad.

Entre las características principales que tiene este sistema de base de datos nos encontramos:

  • Esquema de datos flexible: Se puede alterar el modelo en cualquier momento y almacenar cualquier tipo de string como objetos JSON, ademas de distintas formas de estructura de datos que soporta.
  • No hay relaciones como en una base de datos relacional, debes manejarlas por ti mismo
  • Gran rendimiento y gran escalabilidad al poder montar un cluster de servicios Redis.
  • Desde la versión 2.4 existen dos estrategias para persistir la información en disco.
  • Soporte para una gran cantidad de lenguajes de programación

Casos de Uso de ejemplo de Redis

Algunos ejemplos de casos de uso principales en los que Redis se emplea son los siguientes:

  • Caché de páginas web: Algunos desarrolladores utilizan Redis para almacenar fragmentos de html, acelerando la entrega de estos en una aplicación web.
  • Almacenamiento de sesiones de usuario: Se emplea en numeras ocasiones para almacenar las sesiones de usuario, en un almacén de acceso muy rápido. Eso permite aligerar los tiempos de consulta al servidor. Ademas Redis permite incluir un TTL asociado a la clave, por lo que evita al desarrollador implementar la expiración de la sesión.
  • Almacenamiento de carritos de la compra: En soluciones e-commerce se puede emplear al igual que las sesiones de usuario para almacenar los artículos añadidos en un carrito de compra.
  • Caché de base de datos: Aliviando la consulta a base de datos, almacenando determinados resultados en él, sobre todo si se ejecutan con frecuencia
  • Contadores y estadísticas: Para manejar contadores y estadísticas en tiempo real, manteniendo un soporte de gestión concurrente y atómico. Por ejemplo visualizaciones de productos, votos de usuario.

Para que puedas entender el alcance de Redis algunos ejemplos de empresas que utilizan Redis son las siguientes: Twitter, GitHub, Pinterest, Snapchat, StackOverflow o Trello. En la siguiente url (http://techstacks.io/tech/redis) puedes ver ejemplos de empresas que emplean Redis además de navegar por los distintos stacks de arquitectura de aplicaciones archiconocidas.

Entre otras alternativas a esta tecnología podemos encontrarnos también con Memcached aunque esta última está mas orientada a utilizarse como cache.

Caso de uso especifico

El contexto de nuestro caso de uso es la consulta de líneas de teléfono para un proyecto para una operadora móvil. Desarrollamos una API REST para la consulta de las lineas de un cliente que consumía tanto una aplicación web en React como una aplicación móvil en React Native.

El problema principal se trataba que para construir todos los datos necesarios que tiene una línea (número de teléfono, nombre de la linea, tipo de linea, tarifas asociadas, servicios asociados) debíamos de realizar varias llamadas a un servicio externo. Este servicio tampoco nos ofrecía tenía una llamada única construir toda esta información para todas las lineas de un cliente, si no que debíamos llamar n veces por línea para recuperar toda la información a mostrar.

Este proceso podía tardar en torno a un segundo por línea y aquellos clientes que tenían varias lineas contratadas se veían afectados en el performance. De tal manera implementamos un sistema de caché basado en Redis que permitía mantener ese objeto construido con los datos de la linea accesible en un tiempo razonable, mejorando considerablemente la experiencia de uso tanto de la aplicación como las áreas clientes.

Implementación de Redis en Symfony

Instalación del bundle SNCRedisBundle

Este bundle integra el cliente PHP de Predis dentro de una aplicación Symfony. La configuración del bundle es muy sencilla, solo tendremos que indicar el nombre de la base de datos a utilizar. En Redis las distintas base de datos del sistema se indican con números (redis://secret@localhost/1) :

snc_redis:
clients:
default:
type: predis
alias: default
dsn: 'redis://secret@localhost/1
cache:
type: predis
alias: cache
dsn: 'redis://secret@localhost/1'
options:
profile: 2.2
connection_timeout: 10
read_write_timeout: 3

Además de permitir el acceso al cliente de Redis, tiene utilidades como un handler para las sesiones de Symfony, para la cache de Doctrine o incluso para almacenamiento de Logs. Puedes ver como utilizar estas funcionalidades viendo el github del bundle: https://github.com/snc/SncRedisBundle/blob/master/Resources/doc/index.md

Implementar Servicio que gestiona la cache caché

En la siguiente clase RedisCache se implementa los métodos básicos para utilizar redis como un sistema de caché. Permitiendo almacenar objetos con setObject y recuperarlos con getObject con un TTL especifico. Además se incluye un método para invalidar estos objetos y eliminarlos de la caché.

<?php
namespace AppBundle\Services;
use AppBundle\Interfaces\CacheInterface;
use JMS\Serializer\SerializationContext;
use JMS\Serializer\Serializer;
use Predis\Client;
/**
* Class ScheduleService
* @package AppBundle\Services
*/
class RedisCache implements CacheInterface
{
const TTL_MINUTE=60;
const TTL_HOUR=3660;
const TTL_DAY=86400;
    /**
* @var Client
*/
private $client;
    /**
* @var Serializer
*/
private $serializer;
     public function __construct(Client $client, Serializer $serializer)
{
$this->client = $client;
$this->serializer= $serializer;
}
     /**
* @param $key
* @return array
* @internal param $object
*/
public function get($key)
{
$this->client->get($key);
}
     /**
* @param $key
* @return array
*/
public function invalidate($key)
{
$this->client->del(array($key));
}
    /**
* @param $key
* @param $value
* @param int $ttl
* @return object
*/
public function set($key, $value, $ttl = 0)
{
if($ttl >0) {
$this->client->setex($key, $ttl, $value);
}else{
$this->client->set($key, $value);
}
}
     /**
* @param $key
* @param $value
* @param int $ttl
* @return object
*/
public function setObject($key, $value, $ttl = 0)
{
$json=$this->serializer->serialize($value, 'json', SerializationContext::create()->enableMaxDepthChecks());
$this->set($key, $json, $ttl);
}
    /**
* @param $key
* @param $value
* @param int $ttl
* @return object
*/
public function getObject($key)
{
$object=$this->client->get($key);
if(!$object){
return false;
}
         return $this->serializer->deserialize($object, 'array','json');
}
}

Para utilizar este servicio, se suele consultar primero a la cache si tiene la información que queremos recuperar. Si hacemos hit en el sistema de caché trabajaremos con el objeto cacheado. En caso contrario, deberemos ejecutar el código para obtener la información necesaria. En mi caso se requerían varias llamadas a un servicio externo para construir el objeto deseado.

Una vez tenemos la información guardamos en la caché la información adquirida, con un TTL especifico, para que las próximas consultas evitar estas llamadas al servicio externo y que los sirva el mismo Redis.

$subscription = $this->cache->getObject($subscriptionId);
if (!$subscription) {
     $subscriptionDetailData = $this->getSubscriptionDetailData($subscriptionData);
     $subscription = $this->subscriptionMapper->arrayToObject($subscriptionDetailData);
     $this->subscriptionBuilder->addPackage($subscription, $subscriptionDetailData);
     $this->subscriptionBuilder->addBonusProducts($subscription, $subscriptionDetailData);
     $this->subscriptionBuilder->addThrottle($subscription, $subscriptionDetailData);
    $this->cache->setObject($subscriptionId, $subscription, RedisCache::TTL_DAY);
    $this->cache->setObject($subscriptionId.”detail”, $subscriptionDetailData, RedisCache::TTL_DAY);
}

Muy importante, es tratar las invalidaciones. Debemos invalidar cualquier objeto de la caché cuando se ejecute una acción sobre ellos que pueda cambiar su valor, si no podremos obtener información incoherente con la base de datos maestra. En nuestro caso cualquier llamada a la api como un cambio de tarifa, una contratación de servicio, etc…

$this->get(‘app.cache’)->invalidate($subscriptionId);

Al invalidar solo esa linea, en la siguiente petición para obtener las lineas, la única que tendría que llamar a servicio externo es nuestra linea que ha cambiado, el resto se mantendrían intactas, por lo que apenas se notaría en el performance.

Esta es una de las soluciones que implementamos pero Redis puede ser util para muchos casos de uso de vuestras aplicaciones. Te recomiendo que si en tu aplicación empiezas a tener problemas de rendimiento o hay un gran número de usuarios consultando la misma información te plantees montar algún tipo de sistema que se una manera sencilla alivie el resto de recursos y mejore el performance y la escalabilidad de tu web.

Artículo publicado en: https://adrianalonso.es/desarrollo-web/nosql/utilizando-redis-como-sistema-de-cache-en-symfony/