Dostali sme úlohu. Naprogramovať kuriéra. Nič zvláštne, keby požiadavka nebola poslať v jednom momente aj 20 tisíc správ. Ok, to by sa ešte zvládlo, veď je to len 20 tisíc riadkov v databáze, ktoré musíme vyrobiť. Máme predsa MongoDB bulkWrites a podobné nástroje, ktoré to zvládnu hravo.
Kameň úrazu nastal, keď si klient v ďalšom sprinte zmyslel, že chce o týchto spravách notifikáciu emailom, push notifikáciu a u vyvolených aj SMS správu. Hneď som si predstavil používateľské rozhranie administrátora, ako (pri najlepšom) pár minút čaká na odoslanie správy. Celé zle. Musí na to byť lepšie riešenie.
Redis streams
Ani neviem, prečo mi napadlo pozrieť najnovšiu dokumentáciu Redis.io. A ľaľa. Redis streams, novinka od verzie 5. Šikovná vec. Ale poporiadku.
Redis stream je vlastne list. Alebo zoznam. Ale nie obyčajných položiek, ale hash-ov. Teda štruktúrovaných dát. To je pre mňa jedna z veľkých výhod, nie je potrebné ukladať napríklad json a potom ho pri spracovaní rozkódovať. Šetríme tak procesor, a teda aj energiu ☀️.
Do streamu jednoducho zapisujete položky. Alebo log. Alebo nejaké časové dáta. Alebo v našom prípade zoznam správ, ktoré je potrebné odoslať emailom, push-kou, SMS správou.
My to robíme nasledovne:
XADD spravy_na_odoslanie * messageId 57488552818b284284a28cf5 userId 55a6733f03af1ade801a2231 subject Test
Vytvorili sme si správu do streamu spravy_na_odoslanie s parametrami messageId, userId a subject. Redis každej správe pridelí jednoznačné a jedinečné ID, ktoré pozostáva z časovej značky a inkrementu, teda dva integery: {int}-{int}.
Sila redis streamov, je ale v ich čítaní a konkrétne pri použití GROUPs. Práve pri čítaní cez konzumerov groupy sa redis stará o to, aby každý consumer dostal jednu správu práve raz a aby žiadna správa nebola do grupy doručená viackrát. Grupa je teda skupina konzumerov, alebo teda čitateľov správ. Jedna grupa môže mať viac konzumerov, z ktorých redis vyberie najvhodnejšieho a tomu odošle najnovšiu správu.
Z nášho pohľadu sme teda vytvorili pre každý kanál samostatnú grupu. Pre emaily jednu, pre SMS správy ďalšiu a ďalšiu pre push-ky.
XGROUP CREATE spravy_na_odoslanie courierEmailDigestGroup $
XGROUP CREATE spravy_na_odoslanie courierSmsDigestGroup $
XGROUP CREATE spravy_na_odoslanie courierPushDigestGroup $
Každá grupa má momentálne štyroch konzumerov. Takto prijímajú správy štyrikrát rýchlejšie. Pointa je, že keď nebudeme stíhať, pridáme konzumera. Jednoduché však? Geniálne.
Samotné čítanie správ u nás beží v samostatnom procese pre každého konzumera. Funguje to tak, že pri štarte procesu sa načítajú všetky správy, ktoré sme nedostali. Redis sa rozhodne, ktoré, podľa konzumera a jeho vyťaženia.
XREAD GROUP courierEmailDigestGroup courierEmailDigestConsumer01 COUNT 10 BLOCK 100000 STREAMS spravy_na_odoslanie 0-0
Takto získame všetky správy platné pre grupu courierEmailDigestGroup a jej konzumera courierEmailDigestConsumer01. Načítame si ich 10 a na ďalšie čakáme 10 sekúnd. 0-0 určuje, že chceme správy od ID 0-0. Teda od začiatku. Redis nám samozrejme pošle len tie správy, ktoré nám ešte neodoslal, respektíve len tie, ktorým sme nepotvrdili prijatie. Ak nepríde žiadna správa, čakáme na ďalšie v reálnom čase pomocou príkazu:
XREAD GROUP courierEmailDigestGroup courierEmailDigestConsumer01 COUNT 10 BLOCK 100000 STREAMS spravy_na_odoslanie >
Toto sa opakuje v cykle pre každého konzumera. Takto sme získali tzv. load-balancing, nezávisle od hlavného procesu API dokážeme naraz odoslať aj pushky, aj emaily aj SMS správy a zároveň je to redundantné proti reštartom, redis sa nám o to postará a zároveň máme postarané aj to, že sa jedna správa doručí práve jednému konzumerovi v rámci jednej grupy.
Dôležité je, aby ste po spracovaní správy, v našom prípade po odoslaní emailu dali redisu vedieť, že sme správu úspešne spracovali, aby nám ju už neposielal.
XACK spravy_na_odoslanie courierEmailDigestGroup 1554703280113-0
Redis streams majú celkovo veľa vychytávok, my sme si z toho zobrali zatiaľ tento use-case. Dovolím si tvrdiť, že v malom nahradia Apache Kafka. Viac info sa viete dočítať tu.
Napadajú vás ďalšie príklady použitia? Podeľte sa s nami o ne dole v komentároch :-)