Node.js

Peticiones a caché

     

En este documento vamos a ver cómo mejorar el rendimiento de nuestras aplicaciones web mediante la realización de peticiones a una memoria caché del servidor o caching.

1. Introducción a las peticiones a caché

En aplicaciones web, las peticiones a caché pueden referirse a diferentes tipos de caché. Dos de los más comunes son:

En este apartado nos vamos a centrar en las peticiones a caché del servidor. Este tipo de caché juega un papel importante en el rendimiento de las aplicaciones web modernas. A diferencia de la caché del navegador, que es gestionada automáticamente por el navegador y se limita a recursos estáticos, la caché del servidor puede ser configurada y optimizada de manera precisa para almacenar datos dinámicos y consultas de base de datos, proporcionando beneficios significativos en entornos de alta demanda y aplicaciones complejas.

1.1 Funcionamiento de las peticiones a caché del servidor

Las peticiones a caché del servidor funcionan almacenando temporalmente los datos que se consultan con frecuencia en una memoria rápida o caché, lo que permite acceder a esos datos más rápidamente en futuras solicitudes.

A continuación, se explica el funcionamiento de estas peticiones paso a paso:

Primera petición del cliente:

Peticiones siguientes:

1.2 Ventajas e inconvenientes de las peticiones a caché del servidor

Ventajas

Inconvenientes

2. Introducción a Redis

Redis (REmote DIctionary Server) es un almacén de estructura de datos en memoria, de código abierto, que se utiliza como caché de aplicaciones y/o base de datos NoSQL de respuesta rápida.

Redis sigue una arquitectura cliente-servidor, lo que significa que hay dos componentes principales en su funcionamiento: el servidor Redis y los clientes Redis.

Utiliza un modelo de datos clave-valor, donde las claves son identificadores únicos, cuyo valor puede ser uno de los muchos tipos de datos que admite Redis (cadenas, listas, conjuntos, hashes y más).

Una de sus principales características es que puede ser configurado para trabajar en un entorno distribuido utilizando mecanismos como la replicación y los clústeres de Redis. Esto permite que los datos se repliquen y se distribuyan entre varios nodos, asegurando su disponibilidad y durabilidad. Además, Redis ofrece soporte para diferentes lenguajes de programación, incluyendo Java, Python, PHP, C, C++, C#, Javascript, entre otros.

Otras plataformas similares a Redis

2.1 Instalación de Redis

2.1.1 Instalación en Linux

Para instalar Redis en una distribución basada en Debian como Ubuntu y asegurar que esté funcionando y se inicie automáticamente con el sistema, sigue los siguientes pasos:

  1. Actualizar los paquetes del sistema: sudo apt update
  2. Instalar Redis: sudo apt install redis-server
  3. Iniciar el servicio: sudo systemctl start redis-server
  4. Verificar la instalación:
    • redis-cli ping. Si Redis está funcionando, deberías ver la respuesta PONG.
    • sudo systemctl status redis-server. Este comando muestra información sobre el estado actual del servicio.

Opcionalmente, se puede configurar Redis para que se inicie automáticamente al arrancar el sistema con el siguiente comando: sudo systemctl enable redis-server.

Si surge algún problema al iniciar el servicio, revisa los logs de Redis. Suelen encontrarse generalmente en /var/log/redis/redis-server.log.

El archivo de configuración de Redis generalmente se llama redis.conf y encuentra en /etc/redis/. Es posible editarlo para incluir medidas de seguridad, como establecer una contraseña o limitar las conexiones a direcciones IP confiables. Trataremos esto en un apartado posterior.

2.1.2 Instalación en Windows

Redis no es oficialmente compatible con Windows. Para instalar Redis en Windows, primero hay que instalar WSL2 (Windows Subsystem for Linux). WSL2 permite ejecutar binarios de Linux de forma nativa en Windows. Para que este método de instalación funcione, es necesario disponer de Windows 10 versión 2004 o superior, o Windows 11. Para versiones anteriores, consulta la página de instalación manual.

Paso 1. Instalar WSL2

El proceso de instalación también lo puedes consultar en las instrucciones detalladas para instalar WSL proporcionadas por Microsoft.

Paso 2. Iniciar sesión en WSL

Paso 3. Actualizar y mejorar la distribución

Windows no actualiza ni mejora automáticamente las distribuciones de Linux que tengas instaladas. Es una buena práctica mantener los paquetes de la distribución de Linux actualizados para asegurar un buen rendimiento y seguridad. Para ello, abre tu terminal de WSL y ejecuta los siguientes comandos: sudo apt update y sudo apt upgrade.

Paso 4. Instalación de Redis

Sigue los siguientes pasos para instalar versiones estables recientes de Redis desde el repositorio oficial de APT desde tu terminal WSL. El proceso de instalación también lo puedes consultar en la documentación oficial de Redis.

curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor \
-o /usr/share/keyrings/redis-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] \
https://packages.redis.io/deb $(lsb_release -cs) main" | \
sudo tee /etc/apt/sources.list.d/redis.list

Siguiendo estos pasos, deberías tener Redis instalado y funcionando en tu sistema Windows mediante WSL.

Paso 5. Iniciar y verificar el servicio

Otra forma de comprobar si Redis está activo es utilizando el cliente de Redis para enviar un comando PING y recibir una respuesta PONG con el siguiente comando: redis-cli ping

Opcionalmente, se puede configurar Redis para que se inicie automáticamente al arrancar el sistema con el siguiente comando: sudo systemctl enable redis-server.

Si surge algún problema al iniciar el servicio, es conveniente revisar los logs de Redis, que suelen encontrarse en /var/log/redis/redis-server.log ó reiniciar el servicio con sudo service redis-server restart.

El archivo de configuración de Redis generalmente se llama redis.conf y encuentra en /etc/redis/. Es posible editarlo para incluir medidas de seguridad, como establecer una contraseña o limitar las conexiones a direcciones IP confiables. Trataremos esto en el apartado siguiente.

2.1.3 Operaciones adicionales para la administración de WSL

A continuación, se detallan algunas operaciones adicionales que pueden ser útiles para la administración de tu distribución de Linux en WSL.

Listar las distribuciones instaladas

Para ver todas las distribuciones de Linux instaladas y su estado, utiliza: wsl -l -v

Si tienes instalado Docker Desktop, es posible que esté configurado para que se inicie automáticamente y utilice WSL2 como backend. Esto puede provocar que se inicie Docker Desktop en lugar de Ubuntu. Si esto ocurre, puedes iniciar una distribución concreta con el comando wsl -d <NombreDistro>, que hemos visto antes, o bien modificar la distribución predeterminada, como se explica a continuación.

Establecer una distribución como predeterminada

Si quieres que una determinada distribución sea la predeterminada para que se inicie cada vez que se ejecute wsl, puedes establecerla así: wsl --set-default <NomDistro>.

En este caso, si queremos establecer que Ubuntu sea la distribución predeterminada, ejecutaremos desde PowerShell el siguiente comando: wsl --set-default Ubuntu

Cambiar contraseña

Si quieres cambiar la contraseña de tu distribución predeterminada, sigue estos pasos:

Restablecer la contraseña

Si has olvidado la contraseña de tu distribución predeterminada, sigue estos pasos:

2.2 Configuración de Redis

Una vez instalado, se puede configurar Redis mediante el archivo de configuración redis.conf, que generalmente se encuentra en /etc/redis/. Para editar el archivo de configuración, escribe el siguiente comando: sudo nano /etc/redis/redis.conf en tu terminal WSL.

Una configuración básica, incluye las siguientes medidas de seguridad:

Según esto, la configuración recomendada de redis.conf sería la siguiente:

requirepass tucontraseña
bind 127.0.0.1 -::1
protected-mode yes

Otros mecanismos de seguridad incluyen el uso de TLS (Transport Layer Security) para cifrar el tráfico entre el cliente y el servidor, o el uso de un firewall para limitar el acceso al puerto de Redis solo a las IPs o redes que realmente necesitan acceso, pero esto queda fuera del alcance de este curso.

2.3 Integración de Redis con Nodejs

Para conectarnos a Redis desde una aplicación Node.js, necesitamos una biblioteca cliente de Redis específica para este lenguaje. Los clientes de Redis cumplen con las siguientes funciones:

En el caso de Node.js, existen dos clientes populares para Redis: ioredis y redis. Ambos clientes ofrecen APIs de programación similares, envolviendo cada comando de Redis en una función que podemos llamar desde Node.js.

Utilizaremos ioredis, ya que ofrece soporte nativo para Promises, lo que facilita la escritura de código asincrónico sin necesidad de bibliotecas adicionales. Los métodos de ioredis retornan Promises por defecto, lo que permite el uso de async/await de manera sencilla.

Instalación

Redis es tan fácil de instalar como cualquier otro módulo que queramos incorporar a un proyecto Node. Simplemente necesitamos ejecutar el correspondiente comando npm install en la carpeta del proyecto donde queramos añadirlo (y, previamente, el comando npm init en el caso de que aún no hayamos creado el archivo package.json):

npm install ioredis

Ejemplo básico de uso

Vamos a crear un proyecto llamado “PruebaCaching” en la carpeta de “ProyectosNode/Pruebas”, en el que ejecutaremos comandos básicos SET y GET para almacenar y recuperar valores en la base de datos Redis. Lo primero que tenemos que hacer es instalar Redis, y después crear un archivo index.js con este código:

const redis = require('redis');
const client = redis.createClient();

client.set('key', 'value', redis.print);

client.get('key', (err, reply) => {
  console.log(reply); // 'value'
});

El código, como vemos, es muy sencillo. Primero incluimos la librería y creamos una nueva instancia de un cliente de Redis utilizando el método createClient de la biblioteca Redis. Esto establece una conexión con el servidor de Redis, permitiendo interactuar con la base de datos.

A continuación ejecutamos el comando set de Redis para almacenar el par clave-valor (mi-nombre y may) en la base de datos Redis. Como tercer parámetro, utilizamos una función de callback, propocionada por la biblitoeca Redis, para imprimir el resultado de la operación, redis.print. Esto imprimirá Reply: OK si la operación se realiza con éxito.

Finalmente, ejecutamos el comando get de Redis para recuperar el valor el valor asociado con mi-nombre. Le pasaremos como segundo argumento una función callback para manejar el resultado. Si ocurre un error durante la operación, err contendrá el error (en este caso, no se está manejando explícitamente), y replycontendrá el valor asociado con mi-nombre si la operación tiene éxito. La consola imprimirá may.

2.3.1 Operaciones básicas en Redis

Redis proporciona diversas operaciones básicas, entre ellas:

client.set('key', 'value', redis.print);
client.get('key', (err, reply) => {
  console.log(reply); // 'value'
});
client.del('key', (err, reply) => {
  console.log(reply); // 1 if deleted
});
client.set('counter', 100);
client.incr('counter', (err, reply) => {
  console.log(reply); // 101
});
client.expire('key', 60); // Expira en 60 segundos

2.3.2. Ejemplo de uso de Redis para caché de peticiones
const express = require('express');
const app = express();
const redis = require('redis');
const client = redis.createClient();

// Middleware para cachear respuestas
function cache(req, res, next) {
  const { id } = req.params;

  client.get(id, (err, data) => {
    if (err) throw err;

    if (data !== null) {
      res.send(JSON.parse(data));
    } else {
      next();
    }
  });
}

app.get('/data/:id', cache, (req, res) => {
  const { id } = req.params;
  // Supongamos que fetchData es una función que obtiene datos de la base de datos
  fetchData(id).then((data) => {
    client.setex(id, 3600, JSON.stringify(data)); // Cachea la respuesta por 1 hora
    res.send(data);
  });
});

function fetchData(id) {
  // Simulación de una consulta a base de datos
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ id, data: "Data from database" });
    }, 200);
  });
}

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

4. Estrategias para gestionar la consistencia de datos en caché

Para gestionar adecuadamente los datos en la caché y evitar que se queden obsoletos, podemos utilizar varias estrategias y buenas prácticas. Aquí hay algunas recomendaciones clave:

client.setex('key', 3600, 'value'); // Expira en 1 hora
function updateData(id, newData) {
  // Actualizar datos en la base de datos
  database.update(id, newData).then(() => {
    // Eliminar la entrada de la caché
    client.del(id);
  });
}
function getData(id) {
  return new Promise((resolve, reject) => {
    client.get(id, (err, data) => {
      if (err) reject(err);
      if (data !== null) {
        resolve(JSON.parse(data));
      } else {
        // Datos no están en caché, recuperarlos de la base de datos
        database.get(id).then((result) => {
          // Almacenar los datos en la caché y resolver la promesa
          client.setex(id, 3600, JSON.stringify(result)); // TTL de 1 hora
          resolve(result);
        }).catch(reject);
      }
    });
  });
}
function updateData(id, newData) {
  // Actualizar la caché y la base de datos
  client.set(id, JSON.stringify(newData), (err) => {
    if (err) throw err;
    database.update(id, newData);
  });
}
function getData(id) {
  client.get(id, (err, data) => {
    if (err) throw err;
    if (data) {
      return JSON.parse(data);
    } else {
      database.get(id).then((result) => {
        client.set(id, JSON.stringify(result));
        return result;
      });
    }
  });
}