Kako koristiti Redis za nadopunu web API-ja

Izvedba je važan parametar koji treba uzeti u obzir prilikom dizajniranja bilo kojeg softverskog dijela. Osobito je važno kada je riječ o onome što se događa iza kulisa.

Mi, kao programeri i tehnolozi, usvajamo višestruke dorade i implementacije kako bismo poboljšali performanse. Tu dolazi do izražaja predmemoriranje.

Keširanje je definirano kao mehanizam za pohranu podataka ili datoteka na privremenom mjestu za pohranu odakle mu se može odmah pristupiti kad god je to potrebno.

Keširanje je danas postalo neophodno u web aplikacijama. Redis možemo koristiti za nadopunu naših web API-ja - koji su izrađeni pomoću Node.js i MongoDB.

Redis: Laički pregled

Redis je, prema službenoj dokumentaciji, definiran kao spremište strukture podataka u memoriji koje se koristi kao baza podataka, posrednik poruka ili spremište predmemorije. Podržava podatkovne strukture kao što su nizovi, hashovi, popisi, skupovi, razvrstani skupovi s upitima raspona, bitmape, hiperloglozi, geoprostorni indeksi s upitima radijusa i tokovima.

Ok, to je poprilično puno struktura podataka upravo tamo. Da bi to bilo jednostavno, gotovo sve podržane strukture podataka mogu se sažeti u jedan ili drugi oblik niza. Dobit ćete više jasnoće dok prolazimo kroz provedbu.

Ali jedno je jasno. Redis je moćan i ako se pravilno koristi, naši programi mogu biti ne samo brži, već i nevjerojatno učinkoviti. Dosta razgovora. Uprljajmo ruke.

Razgovarajmo o kodu

Prije nego što krenemo, morat ćete podesiti redis u vašem lokalnom sustavu. Možete slijediti ovaj postupak brzog postavljanja kako biste pokrenuli i pokrenuli redis.

Gotovo? U redu. Počnimo. Imamo jednostavnu aplikaciju izrađenu u Expressu koja koristi instancu u MongoDB Atlas za čitanje i pisanje podataka.

U /blogsdatoteci rute imamo dva glavna API-ja .

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

Prskati malo Redisove dobrote

Počinjemo s preuzimanjem npm paketa redisza povezivanje s lokalnim redis poslužiteljem.

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

Koristimo utils.promisifyfunkciju za transformiranje client.hgetfunkcije za vraćanje obećanja umjesto povratnog poziva. Više o tome možete pročitati promisificationovdje.

Veza s Redisom je uspostavljena. Prije nego što počnemo pisati bilo koji predmemorirani kôd, vratimo se korak unatrag i pokušajmo shvatiti koji su zahtjevi koje moramo ispuniti i vjerojatni izazovi s kojima bismo se mogli suočiti.

Naša strategija predmemoriranja trebala bi biti u stanju riješiti sljedeće točke.

  • Predmemorirajte zahtjev za sve postove na blogu za određenog korisnika
  • Očisti predmemoriju svaki put kad se stvori novi post na blogu

Vjerojatni izazovi na koje bismo trebali biti oprezni tijekom izrade strategije:

  • Pravi način za rukovanje stvaranjem ključa za pohranu podataka predmemorije
  • Logika isteka predmemorije i prisilno isteka radi održavanja svježine predmemorije
  • Višekratna primjena logike predmemoriranja

U redu. Zabilježili smo točke i povezali ih. Na sljedeći korak.

Nadjačavanje zadane funkcije Mongoose Exec

Želimo da se naša logika predmemoriranja može ponovno koristiti. I ne samo za ponovnu upotrebu, mi također želimo da to bude prva kontrolna točka prije nego što uputimo bilo kakav upit u bazu podataka. To se lako može učiniti pomoću jednostavnog hackanja backgy-a na funkciju exec mongoose.

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

Koristimo prototip objekta mongoose da bismo dodali svoj logički kod predmemoriranja kao prvo izvršavanje u upitu.

Dodavanje predmemorije kao upita

Da bismo označili koji upiti trebaju biti predmemorirani, kreiramo upit mungoose. Pružamo mogućnost prolaska datoteke koja će userse koristiti kao hash-ključ kroz optionsobjekt.

Napomena: Hashkey služi kao identifikator strukture raspršenih podataka koji se, laički rečeno, može navesti kao nadređeni ključ skupa parova ključ / vrijednost. Time se omogućuje predmemoriranje većeg broja skupa vrijednosti upita. Više o hashovima u redisu možete pročitati ovdje.
... mongoose.Query.prototype.cache = function(options = {})  'default'); return this; ; ...

Učinivši to, cache()upit možemo koristiti zajedno s upitima koje želimo predmemorirati na sljedeći način.

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

Izrada logike predmemorije

Postavili smo uobičajeni upit za ponovnu upotrebu kako bismo označili koje upite treba predmemorirati. Idemo naprijed i napišite središnju logiku predmemoriranja.

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

Kad god koristimo cache()upit zajedno s našim glavnim upitom, postavljamo enableCacheključ na true.

Ako je ključ netačan, vraćamo glavni execupit prema zadanim postavkama. Ako nije, prvo oblikujemo ključ za dohvaćanje i spremanje / osvježavanje podataka predmemorije.

collectionIme koristimo zajedno sa zadanim upitom kao ključnim imenom radi jedinstvenosti. Korištena hash-tipka naziv je userkojega smo već postavili ranije u cache()definiciji funkcije.

Predmemorirani podaci dohvaćaju se pomoću client.hget()funkcije koja za parametre zahtijeva hash-ključ i posljedični ključ.

Napomena: Uvijek koristimo JSON.parse()dok dohvaćamo bilo koje podatke iz redisa. Slično tome, koristimo JSON.stringify()ključ i podatke prije nego što bilo što pohranimo u redis. To je učinjeno jer redis ne podržava JSON podatkovne strukture.

Nakon što dobijemo predmemorirane podatke, svaki od predmemoriranih objekata moramo transformirati u model mungoose. To se može učiniti jednostavnim korištenjem 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. Još jednom dohvaćamo sve postove na blogu za korisnika tejaz.

Možete jasno vidjeti da kada se donese iz cache, vrijeme potrebno je sišao s 409ms do 24ms . To nadoplaćuje vaš API smanjujući vrijeme potrebno za gotovo 95%.

Osim toga, jasno možemo vidjeti kako operacije isteka predmemorije i ažuriranja rade kako se očekivalo.

Kompletni izvorni kod možete pronaći u redis-expressmapi ovdje.

tarique93102 / article-snippets Repozitorij koji sadrži prototipove aplikacija i isječke koda koji se odnose na širenje koncepata - tarique93102 / article- snippets tarique93102 GitHub

Zaključak

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.