Redis-välimuisti Node.js:ssä

Kaipasin Node.js-sovellukseen välimuistia. Minulla on lautapelikirjanpitoon Gamestats-sovellus, jossa on Mongo-tietokanta, Node.js-backend ja React-frontti. Dataa on kertynyt aika lailla, kun kirjanpidossa on kaikki pelatut lautapelit parinkymmenen vuoden ajalta, joten osa sivunlatauksista on toivottoman hitaita.

Ongelmia aiheuttaa sekin, että kaikkea datan yhdistelyä ei voi tehdä tietokannassa, vaan monissa tapauksissa on ensin imaistava Mongosta kaikki pelit ja pelisessiot ja sitten käytävä kaikki sessiot läpi ja laskeskeltava niistä kaikenlaista summadataa peleille. Tässä kaikessa kestää, sivunlataukset voivat hyvinkin olla 10–15 sekunnin luokkaa.

Tieto kuitenkin muuttuu melko harvakseen, joten laskutoimitusten tulosten välimuistittaminen on ilmeinen ratkaisu. Tuumin ensin välimuistin tekemistä Mongoon, mutta sitten tuli mieleen, että asiaanhan on ihan varta vasten tehtyjä ratkaisuja ja Redis oli tuttu WordPress-puolelta.

Katsoin ja Heroku tarjoaa ilmaista Redistä, joten mikäpä jottei – katsoin että on aika opetella, miten Redistä käytetään Nodessa.

Otetaan ensin kuitenkin avainkysymys: kannattiko? No tottahan toki. Tässä vähän lokeja localhostilta:

 <-- GET /api/games/playgoal?goal=50
 --> GET /api/games/playgoal?goal=50 200 5,668ms 6.53kb # Ilman Redistä
 <-- GET /api/games/playgoal?goal=50
 --> GET /api/games/playgoal?goal=50 200 3ms 6.53kb     # Rediksellä
   
 <-- GET /api/games/firstplays
 --> GET /api/games/firstplays 200 17s 82.92kb          # Ilman Redistä
 <-- GET /api/games/firstplays
 --> GET /api/games/firstplays 200 14ms 82.92kb         # Rediksellä

Seitsemäntoista sekunnin vaihtuminen 14 millisekuntiin on hyvinkin vaivannäön arvoinen asia!

Redis Herokuun

Redis asentuu Herokuun kovin helposti:

heroku addons:create heroku-redis:hobby-dev

Vaihtoehtoisesti Rediksen saa aktivoitua myös käyttöliittymän kautta.

Redis Nodeen

Redis asentuu Nodeen näin helposti:

npm install -S async-redis

Peruspaketti olisi redis, mutta suosittelen tarttumaan heti tuohon async-redisiin, joka kääräisee ympärille async/await-tuen.

Redis otetaan koodissa käyttöön näin:

const redis = require("async-redis")
const redisClient = redis.createClient(process.env.REDIS_URL)

Nyt jos oletetaan, että työskennellään backendin parissa ja halutaan säilöä Redikseen backendin palauttamaa dataa, niin koska Redis on avain-arvo–säilö, tarvitaan avain. Avaimen pitäisi olla yksilöllinen, eli useimmissa tapauksissa se kannattaa muodostaa jollain tapaa siitä kyselystä, joka backendille tulee.

Minä teen sen esimerkiksi näin:

const key = md5(
	"getgames" +
		JSON.stringify(sessionParams) +
		JSON.stringify(countForParents) +
		JSON.stringify(ctx.request.query.output)
)

Tässä käytetään ensinnäkin md5-kirjastoa muodostamaan sekalaisesta materiaalista vähän tiiviimpi merkkijono; tekniikka on vapaa, mutta suositeltava. Avain muodostetaan tässä seuraavista osista: ensin on polun osoittava getgames, sitten pari omaa parametriä (jotka muodostetaan tietyllä tapaa kyselyparametreistä) ja sitten yksi kyselyparametri sellaisenaan. Näistä kaikista otetaan merkkijonoesitys, vedetään ne nippuun ja koko sotku md5:n läpi. Tuloksena on jotain tämänkaltaista: b471fabf1a2b292bdc4da48434039964.

Periaatteessa kai voisi vetäistä vain koko ctx.request.query-objektin merkkijonoksi ja md5:n läpi, mutta kun en ole ihan varma onko siinä minun käyttämieni parametrien lisäksi jotain ylimääräistä muuttujaa mukana, niin otan tuosta vain nuo parametrit, joita oikeasti tarvitaan.

Tämä tekee nyt sen, että jos joku parametri muuttuu, niin cachesta ei tule vääriin parametreihin liittyvää dataa vastaukseksi.

Tässä yksinkertainen esimerkki:

const redisGames = await redisClient.get("gamenames")
if (redisGames) {
	ctx.body = JSON.parse(redisGames)
} else {
	const games = await Game.find({})
	const gameNames = games.map(game => game.name)
	redisClient
		.set("gamenames", JSON.stringify(gameNames))
		.catch(error => console.log("redis error", error))
	ctx.body = gameNames
}

Tämä on yksi kokonainen polku API:ssa, varsin yksinkertainen koska mitään parametrejä ei käsitellä. Tämä palauttaa vain kaikkien kannasta löytyvien pelien nimet. Aineisto yritetään ensin hakea Rediksestä. Jos redisGames on määritelty, data löytyi ja se voidaan parsia JSON-muodosta auki ja palauttaa sellaisenaan.

Jos Rediksestä ei löytynyt mitään, sitten haetaan data Mongosta, manipuloidaan Mongon palauttama data haluttuun muotoon ja pistetään sitten valmis paketti Redikseen talteen JSON-muodossa.

Redikseen on mahdollista määritellä cachelle vanhenemisaika, mutta itse suosin tässä yhteydessä toisenlaista menetelmää. Data vanhenee harvakseltaan, mutta haluan toisaalta aina nähdä tuoretta dataa (jos muutoksia tulee tiuhaan ja toisaalta ei haittaa, että joskus näkyy vanhaa dataa, toisenlaiset ratkaisut ovat parempia).

Niinpä yksinkertaisesti huuhdon kaikki cachet aina kun dataan tehdään muutoksia:

redisClient.flushall().catch(error => console.log("redis error", error))

Tässä tapauksessa tämä on helppo ja vaivaton ratkaisu, koska kaikki data liittyy yhteen jollain tapaa, useimmissa tapauksissa kaikki cachet pitäisi kuitenkin tyhjentää. Laajemmissa tapauksissa täytyy ehkä olla valikoivampi, tai käyttää erääntymisaikapohjaisia ratkaisuja (näitä md5-tekniikalla tehtyjä avaimia on vähän hankalaa lähteä tyhjentämään yksitellen, koska niistä ei tiedä, mitä ne sisältävät).

Kotitehtäväksi jää virheenhallintaa, näillä keinoin sovellus kaatuu heti, jos Redis-palvelin ei syystä tai toisesta ole saatavilla.

Redis omalle koneelle

Rediksen käyttöönotto Herokussa aiheutti sen ongelman, että asiat menivät rikki omalla koneella testiympäristössä. Koodin voisi varmaan kirjoittaa sen verran fiksusti, että se toimisi silloinkin kun Redistä ei ole saatavilla, mutta miksei saman tien asentaisi Redistä omalle koneelle?

Macilla Redis-asennus on helppo juttu:

brew install redis

Ainakin teoriassa. Tämä nimittäin päättyi verrattain kryptiseen virheilmoitukseen:

Warning: The post-install step did not complete successfully
You can try again using brew postinstall redis

Uudelleen yrittäminen ei auttanut, mutta lisätulosteiden käyttöönotto sen sijaan auttoi, eli brew postinstall redis -v --debug. Kävi ilmi, että ongelma oli tiedosto-oikeuksissa:

Errno::EACCES: Permission denied @ dir_s_mkdir - /usr/local/var/db

Joten ei muuta kuin hakemistoon /usr/local/var/ ja luomaan hakemisto db ja sille vielä omistajaksi oma käyttäjätunnus. Sen jälkeen brew postinstall redis sai vietyä asennuksen maaliin ja redis-server käynnisti Rediksen nätisti ja kaikki vain toimi, mitään ympäristömuuttujia tai mitään ei tarvinnut säätää, oletusasetukset menivät läpi sellaisenaan.

Vastaa

Sähköpostiosoitettasi ei julkaista. Pakolliset kentät on merkitty *

This site uses Akismet to reduce spam. Learn how your comment data is processed.