Cómo utilizar Redis para potenciar sus API web

El rendimiento es un parámetro esencial a considerar al diseñar cualquier pieza de software. Es particularmente importante cuando se trata de lo que sucede detrás de escena.

Nosotros, como desarrolladores y tecnólogos, adoptamos múltiples ajustes e implementaciones para mejorar el rendimiento. Aquí es donde entra en juego el almacenamiento en caché.

El almacenamiento en caché se define como un mecanismo para almacenar datos o archivos en una ubicación de almacenamiento temporal desde donde se puede acceder instantáneamente cuando sea necesario.

El almacenamiento en caché se ha convertido en una necesidad en las aplicaciones web hoy en día. Podemos usar Redis para potenciar nuestras API web, que se crean con Node.js y MongoDB.

Redis: descripción general de un laico

Redis, según la documentación oficial, se define como un almacén de estructura de datos en memoria que se utiliza como base de datos, intermediario de mensajes o almacenamiento en caché. Admite estructuras de datos como cadenas, hashes, listas, conjuntos, conjuntos ordenados con consultas de rango, mapas de bits, hiperloglogs, índices geoespaciales con consultas de radio y flujos.

De acuerdo, ahí hay bastantes estructuras de datos. Para simplificarlo, casi todas las estructuras de datos admitidas se pueden condensar en una forma de cadena u otra. Obtendrá más claridad a medida que avanzamos en la implementación.

Pero una cosa está clara. Redis es poderoso y cuando se usa correctamente puede hacer que nuestras aplicaciones no solo sean más rápidas sino también increíblemente eficientes. Basta de hablar. Ensuciemos nuestras manos.

Hablemos de código

Antes de comenzar, necesitará configurar redis en su sistema local. Puede seguir este proceso de configuración rápida para poner en funcionamiento Redis.

¿Hecho? Frio. Empecemos. Tenemos una aplicación simple creada en Express que hace uso de una instancia en MongoDB Atlas para leer y escribir datos.

Tenemos dos API principales creadas en el /blogsarchivo de ruta.

... // GET - Fetches all blog posts for required user blogsRouter.route('/:user') .get(async (req, res, next) => { const blogs = await Blog.find({ user: req.params.user }); res.status(200).json({ blogs, }); }); // POST - Creates a new blog post blogsRouter.route('/') .post(async (req, res, next) => { const existingBlog = await Blog.findOne({ title: req.body.title }); if (!existingBlog) { let newBlog = new Blog(req.body); const result = await newBlog.save(); return res.status(200).json({ message: `Blog ${result.id} is successfully created`, result, }); } res.status(200).json({ message: 'Blog with same title exists', }); }); ...

Espolvorear algo de bondad Redis

Comenzamos descargando el paquete npm redispara conectarnos al servidor local de redis.

const mongoose = require('mongoose'); const redis = require('redis'); const util = require('util'); const redisUrl = 'redis://127.0.0.1:6379'; const client = redis.createClient(redisUrl); client.hget = util.promisify(client.hget); ...

Hacemos uso de la utils.promisifyfunción para transformar la client.hgetfunción para devolver una promesa en lugar de una devolución de llamada. Puedes leer más sobre promisificationaquí.

La conexión de Redis está en su lugar. Antes de comenzar a escribir más código de almacenamiento en caché, retrocedamos un paso e intentemos comprender cuáles son los requisitos que debemos cumplir y los posibles desafíos que podríamos enfrentar.

Nuestra estrategia de almacenamiento en caché debería poder abordar los siguientes puntos.

  • Almacene en caché la solicitud de todas las publicaciones de blog para un usuario en particular
  • Borrar caché cada vez que se crea una nueva publicación de blog

Los posibles desafíos de los que debemos tener cuidado a medida que avanzamos en nuestra estrategia son:

  • La forma correcta de manejar la creación de claves para almacenar datos de caché
  • Lógica de caducidad de la caché y caducidad forzada para mantener la actualización de la caché
  • Implementación reutilizable de lógica de almacenamiento en caché

Todo bien. Tenemos nuestros puntos anotados y conectados nuevamente. Pasamos al siguiente paso.

Anulación de la función ejecutiva predeterminada de Mongoose

Queremos que nuestra lógica de almacenamiento en caché sea reutilizable. Y no solo reutilizable, también queremos que sea el primer punto de control antes de realizar cualquier consulta a la base de datos. Esto se puede hacer fácilmente usando un simple truco de piggy-backing en la función ejecutiva mangosta.

... const exec = mongoose.Query.prototype.exec; ... mongoose.Query.prototype.exec = async function() { ... const result = await exec.apply(this, arguments); console.log('Data Source: Database'); return result; } ...

Hacemos uso del objeto prototipo de mangosta para agregar nuestro código lógico de almacenamiento en caché como la primera ejecución en la consulta.

Agregar caché como consulta

Para indicar qué consultas deben almacenarse en caché, creamos una consulta de mangosta. Proporcionamos la capacidad de pasar el userpara ser utilizado como clave hash a través del optionsobjeto.

Nota: Hashkey sirve como un identificador para una estructura de datos hash que, en términos sencillos, se puede establecer como la clave principal de un conjunto de pares clave-valor. De este modo, se habilita el almacenamiento en caché de un mayor número de conjuntos de valores de consulta. Puede leer más sobre hashes en redis aquí.
... mongoose.Query.prototype.cache = function(options = {})  'default'); return this; ; ...

Una vez hecho esto, podemos usar fácilmente la cache()consulta junto con las consultas que queremos almacenar en caché de la siguiente manera.

... const blogs = await Blog .find({ user: req.params.user }) .cache({ key: req.params.user }); ...

Elaboración de la lógica de la caché

Hemos configurado una consulta reutilizable común para indicar qué consultas deben almacenarse en caché. Sigamos adelante y escribamos la lógica de almacenamiento en caché central.

... mongoose.Query.prototype.exec = async function() { if (!this.enableCache) { console.log('Data Source: Database'); return exec.apply(this, arguments); } const key = JSON.stringify(Object.assign({}, this.getQuery(), { collection: this.mongooseCollection.name, })); const cachedValue = await client.hget(this.hashKey, key); if (cachedValue) { const parsedCache = JSON.parse(cachedValue); console.log('Data Source: Cache'); return Array.isArray(parsedCache) ? parsedCache.map(doc => new this.model(doc)) : new this.model(parsedCache); } const result = await exec.apply(this, arguments); client.hmset(this.hashKey, key, JSON.stringify(result), 'EX', 300); console.log('Data Source: Database'); return result; }; ...

Siempre que usamos la cache()consulta junto con nuestra consulta principal, configuramos la enableCacheclave para que sea verdadera.

Si la clave es falsa, devolvemos la execconsulta principal por defecto. Si no, primero formamos la clave para buscar y almacenar / actualizar los datos de la caché.

Usamos el collectionnombre junto con la consulta predeterminada como nombre clave en aras de la singularidad. La clave hash utilizada es el nombre de la userque ya hemos establecido anteriormente en la cache()definición de la función.

Los datos almacenados en caché se obtienen utilizando la client.hget()función que requiere la clave hash y la clave consiguiente como parámetros.

Nota: Siempre usamos para JSON.parse()obtener datos de redis. Y de manera similar, usamos JSON.stringify()la clave y los datos antes de almacenar cualquier cosa en redis. Esto se hace porque redis no admite estructuras de datos JSON.

Una vez que hemos obtenido los datos almacenados en caché, tenemos que transformar cada uno de los objetos almacenados en caché en un modelo de mangosta. Esto se puede hacer simplemente usando new this.model().

If the cache does not contain the required data, we make a query to the database. Then, having returned the data to the API, we refresh the cache using client.hmset(). We also set a default cache expiration time of 300 seconds. This is customizable based on your caching strategy.

The caching logic is in place. We have also set a default expiration time. Next up, we look at forcing cache expiration whenever a new blog post is created.

Forced Cache Expiration

In certain cases, such as when a user creates a new blog post, the user expects that the new post should be available when they fetche all the posts.

In order to do so, we have to clear the cache related to that user and update it with new data. So we have to force expiration. We can do that by invoking the del() function provided by redis.

... module.exports = { clearCache(hashKey) { console.log('Cache cleaned'); client.del(JSON.stringify(hashKey)); } } ...

We also have to keep in mind that we will be forcing expiration on multiple routes. One extensible way is to use this clearCache() as a middleware and call it once any query related to a route has finished execution.

const { clearCache } = require('../services/cache'); module.exports = async (req, res, next) => { // wait for route handler to finish running await next(); clearCache(req.body.user); } 

This middleware can be easily called on a particular route in the following way.

... blogsRouter.route('/') .post(cleanCache, async (req, res, next) => { ... } ...

And we are done. I agree that was a quite a lot of code. But with that last part, we have set up redis with our application and taken care of almost all the likely challenges. It is time to see our caching strategy in action.

Redis in Action

We make use of Postman as the API client to see our caching strategy in action. Here we go. Let's run through the API operations, one by one.

  1. We create a new blog post using the /blogs route

2. We then fetch all the blog posts related to user tejaz

3. Recuperamos todas las publicaciones del blog para el usuario tejazuna vez más.

Puede ver claramente que cuando obtenemos datos de la caché, el tiempo necesario se ha reducido de 409 ms a 24 ms . Esto sobrealimenta su API al disminuir el tiempo necesario en casi un 95%.

Además, podemos ver claramente que las operaciones de actualización y expiración de la caché funcionan como se esperaba.

Puede encontrar el código fuente completo en la redis-expresscarpeta aquí.

tarique93102 / article-snippets Repositorio que contiene aplicaciones prototipo y fragmentos de código relacionados con la difusión de conceptos - tarique93102 / article-snippets tarique93102 GitHub

Conclusión

Caching is a mandatory step for any performance-efficient and data-intensive application. Redis helps you easily achieve this in your web applications. It is a super powerful tool, and if used properly it can definitely provide an excellent experience to developers as well as users all around.

You can find the complete set of redis commands here. You can use it with redis-cli to monitor your cache data and application processes.

The possibilities offered by any particular technology is truly endless. If you have any queries, you can reach out to me on LinkedIn.

In the mean time, keep coding.