Sådan bruges Redis til at overbelaste dine web-API'er

Ydeevne er en vigtig parameter, du skal overveje, når du designer et stykke software. Det er især vigtigt, når det kommer til, hvad der sker bag kulisserne.

Vi, som udviklere og teknologer, vedtager flere justeringer og implementeringer for at forbedre ydeevnen. Det er her caching kommer i spil.

Caching er defineret som en mekanisme til at gemme data eller filer på et midlertidigt lagersted, hvorfra det øjeblikkeligt kan tilgås, når det er nødvendigt.

Caching er blevet et must have i webapplikationer i dag. Vi kan bruge Redis til at overbelaste vores web-API'er - som er bygget ved hjælp af Node.js og MongoDB.

Redis: En lægmand oversigt

Redis er ifølge den officielle dokumentation defineret som et lager i datastrukturen i hukommelsen, der bruges som en database, meddelelsesmægler eller cache-lagring. Det understøtter datastrukturer som strenge, hashes, lister, sæt, sorterede sæt med rækkeforespørgsler, bitmaps, hyperloglogs, geospatiale indekser med radiusforespørgsler og streams.

Okay, det er ret mange datastrukturer lige der. Bare for at gøre det enkelt kan næsten alle understøttede datastrukturer kondenseres til en eller anden form for streng. Du får mere klarhed, når vi løber gennem implementeringen.

Men en ting er klar. Redis er kraftfuldt, og når det bruges korrekt, kan det gøre vores applikationer ikke kun hurtigere men utroligt effektive. Nok snak. Lad os få vores hænder beskidte.

Lad os tale kode

Før vi starter, bliver du nødt til at få redis-opsætning i dit lokale system. Du kan følge denne hurtige installationsproces for at komme i gang igen.

Færdig? Fedt nok. Lad os begynde. Vi har en simpel applikation oprettet i Express, der bruger en forekomst i MongoDB Atlas til at læse og skrive data fra.

Vi har to store API'er oprettet i /blogsrutefilen.

... // 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', }); }); ...

Drys noget Redis godhed

Vi starter med at downloade npm-pakken for redisat oprette forbindelse til den lokale redis-server.

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); ...

Vi bruger utils.promisifyfunktionen til at transformere client.hgetfunktionen til at returnere et løfte i stedet for en tilbagekaldelse. Du kan læse mere om promisificationher.

Redis-forbindelsen er på plads. Inden vi begynder at skrive mere cachekode, lad os tage et skridt tilbage og prøve at forstå, hvad er de krav, vi skal opfylde, og de sandsynlige udfordringer, vi måske står over for.

Vores cachestrategi skal være i stand til at adressere følgende punkter.

  • Cache anmodningen om alle blogindlæg for en bestemt bruger
  • Ryd cache hver gang et nyt blogindlæg oprettes

De sandsynlige udfordringer, som vi bør være forsigtige med, når vi følger vores strategi, er:

  • Den rigtige måde at håndtere nøgleoprettelse til lagring af cachedata på
  • Cache-udløbslogik og tvungen udløb for at opretholde cache-friskhed
  • Genanvendelig implementering af cachelogik

Okay. Vi har vores point nedskrevet og redis forbundet. Gå videre til næste trin.

Tilsidesættelse af Standard Mongoose Exec-funktionen

Vi ønsker, at vores cachelogik kan genbruges. Og ikke kun genanvendelig, vi ønsker også, at det skal være det første kontrolpunkt, før vi foretager nogen forespørgsel til databasen. Dette kan let gøres ved at bruge et simpelt hack af piggy-backing på mangoose exec-funktionen.

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

Vi bruger prototypeobjektet af mangoose til at tilføje vores cache-logik kode som den første udførelse i forespørgslen.

Tilføjelse af cache som en forespørgsel

For at angive, hvilke forespørgsler der skal op til cache, opretter vi en mango-forespørgsel. Vi giver mulighed for at videregive den, userder skal bruges som en hash-nøgle gennem optionsobjektet.

Bemærk: Hashkey fungerer som en identifikator for en hash-datastruktur, som i lægmæssige termer kan angives som overordnet nøgle til et sæt nøgleværdipar. Derved muliggør caching af et større antal forespørgselsværdisæt. Du kan læse mere om hashes in redis her.
... mongoose.Query.prototype.cache = function(options = {})  'default'); return this; ; ...

Når vi har gjort det, kan vi nemt bruge cache()forespørgslen sammen med de forespørgsler, vi vil cache på følgende måde.

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

Udformning af cache-logikken

Vi har oprettet en fælles genanvendelig forespørgsel for at angive, hvilke forespørgsler der skal cachelagres. Lad os fortsætte og skrive den centrale cachelogik.

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

Når vi bruger cache()forespørgslen sammen med vores hovedforespørgsel, indstiller vi enableCachenøglen til at være sand.

Hvis nøglen er falsk, returnerer vi hovedforespørgslen execsom standard. Hvis ikke, danner vi først nøglen til hentning og lagring / opdatering af cachedataene.

Vi bruger collectionnavnet sammen med standardforespørgslen som nøglenavn for unikhedens skyld. Den anvendte hash-nøgle er navnet på det, usersom vi allerede har indstillet tidligere i cache()funktionsdefinitionen.

De cachelagrede data hentes ved hjælp af den client.hget()funktion, der kræver hash-nøglen og den deraf følgende nøgle som parametre.

Bemærk: Vi bruger altid, JSON.parse()mens vi henter data fra redis. Og på samme måde bruger vi JSON.stringify()på nøglen og dataene, før vi gemmer noget i redis. Dette gøres, da redis ikke understøtter JSON-datastrukturer.

Når vi først har fået de cachelagrede data, er vi nødt til at omdanne hvert af de cachelagrede objekter til en Mongoose-model. Dette kan gøres ved blot at bruge 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. Vi henter alle blogindlæg til brugeren tejazigen.

Du kan tydeligt se, at når vi henter fra cachen, er den taget tid gået fra 409ms til 24ms . Dette overbelaster din API ved at reducere tiden med næsten 95%.

Plus, vi kan tydeligt se, at cacheudløb og opdateringsoperationer fungerer som forventet.

Du kan finde den komplette kildekode i redis-expressmappen her.

tarique93102 / artikel-uddrag Repository indeholdende prototype applikationer og kodestykker relateret til formidling af koncepter - tarique93102 / artikel-uddrag tarique93102 GitHub

Konklusion

Caching er et obligatorisk trin til enhver præstationseffektiv og dataintensiv applikation. Redis hjælper dig med let at opnå dette i dine webapplikationer. Det er et superkraftigt værktøj, og hvis det bruges korrekt, kan det helt sikkert give en fremragende oplevelse til udviklere såvel som brugere overalt.

Du kan finde det komplette sæt redis-kommandoer her. Du kan bruge den til redis-cliat overvåge dine cachedata og applikationsprocesser.

Mulighederne for enhver bestemt teknologi er virkelig uendelige. Hvis du har spørgsmål, kan du kontakte mig på LinkedIn.

I mellemtiden skal du fortsætte med at kode.