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 /blogs
datoteci 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 redis
za 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.promisify
funkciju za transformiranje client.hget
funkcije za vraćanje obećanja umjesto povratnog poziva. Više o tome možete pročitati promisification
ovdje.
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 user
se koristiti kao hash-ključ kroz options
objekt.
... 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 enableCache
ključ na true.
Ako je ključ netačan, vraćamo glavni exec
upit prema zadanim postavkama. Ako nije, prvo oblikujemo ključ za dohvaćanje i spremanje / osvježavanje podataka predmemorije.
collection
Ime koristimo zajedno sa zadanim upitom kao ključnim imenom radi jedinstvenosti. Korištena hash-tipka naziv je user
kojega 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č.
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.
- 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-express
mapi ovdje.


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.