Obsah
1. Databáze Redis (nejenom) pro vývojáře používající Python (dokončení)
2. Odpovědi serveru vracející pole (array)
4. Komunikace mezi subsystémy s využitím zpráv
5. Podpora pro paradigma publish-subscribe v Redisu
6. Otestování publikování a odběru zpráv z konzole Redisu
7. Použití paradigmatu publish-subscribe z Pythonu
8. Naprogramování handleru pro čtení publikovaných zpráv
9. Ignorování zpráv o přihlášení a odhlášení z kanálu
10. Chování v případě, že je klient odebírající zprávy pomalý
12. Protokol použitý pro komunikaci se serverem
14. Odpověď obsahující jednoduchý řetězec
15. Odpověď vrácená serverem v případě chyby
16. Odpověď obsahující celé číslo (integer)
17. Odpověď s dlouhým řetězcem resp. blokem bajtů
18. Odpověď obsahující pole hodnot
19. Repositář s demonstračními příklady
1. Databáze Redis (nejenom) pro vývojáře používající Python (dokončení)
V úvodním článku o projektu Redis jsme se seznámili se základními vlastnostmi tohoto systému (v režimu, kdy je Redis provozován na jediném stroji). Taktéž jsme si popsali základní datové typy, s nimiž tato databáze dokáže pracovat a které tedy mohou používat klientské programy při ukládání a načítání údajů do/z Redisu. V závěrečných kapitolách byl zmíněn způsob volání funkcí Redisu z aplikací vyvinutých v programovacím jazyku Python. Dnes budeme v popisu Redisu pokračovat. Ukážeme si zejména způsob použití paradigmatu publish-subscribe, který může být v některých případech velmi užitečný, a taktéž se zmíníme o protokolu, který se používá při komunikaci mezi klientem a serverem Redisu. Vše bude opět vysvětleno jak na příkladech spouštěných přímo z interaktivní konzole Redisu, tak i z programů/skriptů napsaných v Pythonu (tyto programy využívají knihovnu redis-py).
2. Odpovědi serveru vracející pole (array)
Nejdříve si připomeneme, jakým způsobem se vlastně v interaktivní konzoli vypisuje seznam hodnot, protože právě se seznamy hodnot budeme často pracovat v dalším textu. V dokumentaci Redisu se ovšem namísto pojmu seznam (list) používá spíše pojem pole (array) ve chvíli, kdy se nepíše o datové struktuře uložené v databázi, ale o formátu odpovědi serveru klientům. Práce se seznamy uloženými v databázi a poli v odpovědích serveru je ve skutečnosti značně přímočará. Přímo v konzoli Redisu (redis-cli) vytvoříme nový seznam, do něhož vložíme několik hodnot (prvků):
127.0.0.1:6379> rpush seznam1 prvni (integer) 1 127.0.0.1:6379> rpush seznam1 druhy (integer) 2 127.0.0.1:6379> rpush seznam1 treti (integer) 3 127.0.0.1:6379> rpush seznam1 ctvrty (integer) 4
Pokud budeme chtít získat větší množství hodnot (prvků) z tohoto seznamu, můžeme použít například příkaz lrange (list range), kterému zadáme nejnižší a nejvyšší index prvku:
127.0.0.1:6379> lrange seznam1 0 1000 1) "prvni" 2) "druhy" 3) "treti" 4) "ctvrty"
Povšimněte si, jakým způsobem jsou v tomto případě jednotlivé prvky vráceny: před každým prvkem je uveden jeho index (nikoli v původním seznamu, ale ve vráceném poli!). Samozřejmě vrácené pole nemusí přesně odpovídat seznamu uloženému v databázi, protože si prvky můžeme vybírat:
127.0.0.1:6379> lrange seznam1 2 5 1) "treti" 2) "ctvrty"
Prázdné pole se vrátí ve formátu:
127.0.0.1:6379> lrange seznam1 100 200 (empty list or set)
3. Hodnoty nil
V některých případech musí Redis pracovat s hodnotami typu Null nebo též nil. Samotná konzole Redisu dokáže podle kontextu rozpoznat, jakým způsobem se tyto hodnoty mají zobrazit uživateli. Ukažme si to na jednoduchém příkladu s množinami. Nejprve vytvoříme novou množinu s a přidáme do ní jeden prvek. Následně se dotážeme na všechny prvky této množiny:
127.0.0.1:6379 >sadd s 42 (integer) 1 127.0.0.1:6379 >smembers s 1) "42"
V dalších dvou krocích prvek odstraníme a znovu se budeme snažit získat všechny prvky množiny s:
127.0.0.1:6379 >srem s 42 (integer) 1 127.0.0.1:6379 >smembers s (empty list or set)
Podobně Redis zareaguje při čtení prvku z prázdného seznamu:
127.0.0.1:6379 >rpush lst 1 (nil) 127.0.0.1:6379 >rpop lst "10" 127.0.0.1:6379 >rpop lst (nil)
V tomto případě konzole správně zareagovala a napsala informaci o prázdném seznamu nebo množině.
Jak jsou však tyto speciální hodnoty uloženy interně, například v seznamu? Redis v tomto případě může použít řetězec s délkou –1, což je ovšem odlišné od řetězce s nulovou délkou (sémantika je jiná). Podrobnosti se dozvíme ve druhé části článku, v níž si popíšeme komunikační protokol používaný Redisem.
4. Komunikace mezi subsystémy s využitím zpráv
Jedna z velmi užitečných technologií, kterou najdeme v Redisu, je technologie implementující paradigma publish-subscribe (nebo též publisher-subscriber). Jedná se o jednu z forem posílání zpráv mezi několika subsystémy, které tak mohou pracovat relativně samostatně, mohou být nakonfigurovány a administrovány nezávisle na sobě a případná změna architektury může být snadnější, než kdyby byly subsystémy propojeny přímo (například přes binární API). V praxi se setkáme jak s paradigmatem publish-subscribe, tak i s frontami zpráv, ovšem mezi oběma technologiemi existuje několik rozdílů a každá se proto používá k odlišným účelům.
Typické vlastnosti front zpráv:
- Existuje jeden či několik zdrojů zpráv.
- Příjemců může být taktéž více, ovšem zpráva je typicky získána jen jedním z nich.
- Obecně není zaručeno pořadí doručení zpráv.
- Zpráva je zpracována jen jedenkrát, ovšem pokud ji příjemce nezpracuje, může být doručena dalšímu příjemci.
- Volitelná vlastnost související s předchozím bodem: po nezpracování se zpráva vrací zpět do fronty
Fronty zpráv se používají velmi často například ve chvíli, kdy se zpracovávají různé transakce, u nichž není nutné, aby jejich výsledek uživatel viděl v reálném čase. Do fronty se pouze uloží všechny informace o tom, jaká transakce se má provést a později si tuto operaci z fronty vyzvedne nějaký „worker“.
Typické vlastnosti publish-subscribe:
- Existuje jeden či několik zdrojů zpráv.
- Příjemců může být taktéž více, zpráva je doručena všem příjemcům, kteří se k odběru přihlásili.
- Pořadí zpráv je zaručeno.
- Většinou není zaručeno zpracování a ani přijetí zprávy (pokud se příjemce odpojí, zbytku „pipeline“ to nevadí).
Toto paradigma se může použít například při implementaci různých komunikačních systémů atd. Příkladem může být chat, kde záleží na pořadí doručení zpráv a příjemců je většinou větší množství.
5. Podpora pro paradigma publish-subscribe v Redisu
V Redisu nalezneme následujících šest příkazů, kterými je implementováno právě paradigma publish-subscribe:
Příkaz | Stručný popis příkazu |
---|---|
SUBSCRIBE | přihlášení se k odebírání jednoho kanálu nebo většího množství kanálů |
UNSUBSCRIBE | opak předchozího, odhlášení se z odebírání specifikovaných kanálů (popř. ze všech kanálů) |
PSUBSCRIBE | odpovídá SUBSCRIBE, ovšem pro jméno kanálu lze použít žolíkové znaky |
PUNSUBSCRIBE | odpovídá UNSUBSCRIBE, ovšem pro jméno kanálu lze použít žolíkové znaky |
PUBLISH | publikování zprávy do zvoleného kanálu |
PUBSUB | získání podrobnějších informací o stavu kanálů, přihlášených odebíratelů zpráv atd. |
U příkazů psubscribe a punsubscribe je možné ve jménu kanálu používat takzvané žolíkové znaky, které s velkou pravděpodobností znáte například z BASHe při specifikaci souborů. Mezi tyto znaky patří především hvězdička (nahrazuje libovolně dlouhou sekvenci znaků), otazník (nahrazuje jeden libovolný znak) a zápis množiny znaků: [znaky]. Žolíkové znaky se odlišují od zápisu regulárních výrazů především v tom, že „*“ a „?“ před sebou neobsahují specifikaci, jakých znaků se náhrada týká (tj. nepíše se například „.*“ ale jen „*“). Více informací je uvedeno například na stránce https://en.wikipedia.org/wiki/Glob_(programming).
6. Otestování publikování a odběru zpráv z konzole Redisu
Příkazy, které jsme si ve stručnosti popsali v předchozí kapitole, si nyní můžeme poměrně snadno vyzkoušet. Vzhledem k tomu, že mezi sebou budou komunikovat dva klienti (v praxi dvě klientské aplikace, nebo i větší množství aplikací), bude náš příklad používat trojici terminálů:
- V prvním terminálu bude spuštěn redis-server. Můžeme zde povolit logování.
- Ve druhém terminálu spustíme první konzoli Redisu.
- Ve třetím terminálu spustíme druhou konzoli Redisu, takže ji budeme moci snadno ovládat nezávisle na konzoli první.
Spuštění samotného serveru Redisu je ve skutečnosti velmi snadné, o čemž jsme se mohli přesvědčit minule. Takže si jen ve stručnosti připomeňme, že budeme používat konfigurační soubor uložený do adresáře ~/redis. A přímo z tohoto adresáře Redis spustíme:
$ cd ~/redis $ redis-server redis.conf
Jak jsme si již řekli v předchozím textu, spustíme v dalším terminálu konzoli Redisu, do které budeme moci interaktivně zapisovat příkazy a zobrazovat si jejich výstup:
$ redis-cli
Zcela stejným způsobem bude spuštěna druhá interaktivní konzole v dalším (v pořadí již třetím) terminálu:
$ redis-cli
Nyní si konečně můžeme vyzkoušet vzájemnou komunikaci mezi oběma klienty (které nám nahrazují nějaké skutečné klientské aplikace). V první konzoli napíšeme příkaz pro přihlášení ke kanálu, který pro jednoduchost nazveme „kanál1“. Pro přihlášení k odběru zpráv z kanálu se používá příkaz subscribe, kterému musíme předat jméno kanálu:
127.0.0.1:6379> subscribe kanal1 Reading messages... (press Ctrl-C to quit) 1) "subscribe" 2) "kanal1" 3) (integer) 1
Povšimněte si, že se tento příkaz, na rozdíl od všech příkazů, které jsme si až doposud uvedli, neukončil, ale konzole se namísto toho přepnula do režimu čekání na data, která někdo na kanál „kanal1“ pošle. Dokonce jsme již dostali první zprávu typu „subscribe“, která nás informuje o tom, že jsme se přihlásili k odebírání zpráv z kanálu „kanal1“.
V další konzoli můžeme začít publikovat zprávy do různých kanálů. Opět stojí za povšimnutí, že žádný z příkazů Redisu vlastně nekončí nějakou závažnou chybou, a to ani když použijeme neexistující kanál – pouze se vrátí hodnota 0, která značí, že si zprávu nikdo nepřevzal:
127.0.0.1:6379> publish kanal2 zprava (integer) 0 127.0.0.1:6379> publish kanal1 zprava (integer) 1 127.0.0.1:6379> publish kanal1 zprava (integer) 1 127.0.0.1:6379> publish kanal3 zprava (integer) 0
Zpráv můžeme ve zvoleném kanálu publikovat libovolné množství a všechny budou zpracovány klientem, který si zaregistroval odebírání těchto zpráv.
1) "message" 2) "kanal1" 3) "zprava" 1) "message" 2) "kanal1" 3) "zprava"
Příkazem pubsub channels můžeme získat informaci o tom, které kanály jsou v daném okamžiku používány, přesněji řečeno, které kanály mají odebíratele (kanál bez odebíratele odpovídá svým chováním zařízení /dev/null, tj. lze do něj posílat zprávy, které se však ztratí):
127.0.0.1:6379 >pubsub channels 1) "kanal1"
Dalším užitečným příkazem je příkaz pubsub numsub pro zjištění počtu odebíratelů nějakého zvoleného kanálu:
127.0.0.1:6379 >pubsub numsub kanal1 1) "kanal1" 2) (integer) 1
Můžeme uvést i více kanálů, včetně kanálů neexistujících či nepoužívaných:
127.0.0.1:6379 >pubsub numsub kanal1 kanal2 kanal3
1) „kanal1“
2) (integer) 1
3) „kanal2“
4) (integer) 0
5) „kanal3“
6) (integer) 0
V konzoli, kde běží příjemce zpráv, nyní příkaz subscribe ukončíme způsobem, který nám nabídl přímo klient – stlačením klávesové zkratky Ctrl+C:
^C $
Nyní bude publikace zpráv na kanál „kanal1“ končit sice korektně, ale bude se vracet nula, která značí, že si zprávu již nepřevzal žádný příjemce:
127.0.0.1:6379> publish kanal1 zprava (integer) 0
7. Použití paradigmatu publish-subscribe z Pythonu
Předchozí příklady, které jsme přímo zadávali do dvojice interaktivních konzolí Redisu, si samozřejmě můžeme přepsat do Pythonu, protože současná verze knihovny redis-py přístup publisher-subscriber podporuje.
První skript po svém spuštění opublikuje na kanálu „kanal1“ deset zpráv, přičemž každá zpráva bude ve formátu „zprava #n“ s proměnnou hodnotou n. Mezi publikací jednotlivých zpráv bude pauza o délce přibližně jedné sekundy. Samotná publikace je jednoduchá – použijeme přímo metodu publish třídy Redis (či některé třídy odvozené).
Úplný zdrojový kód tohoto příkladu:
import redis import time CHANNEL_NAME = "kanal1" def connect(host, port): return redis.Redis(host=host, port=port) def pub(host, port, channel): r = connect(host, port) for i in range(0, 11): print("Publishing message to " + channel) message = "zprava #{}".format(i) r.publish(channel, message) time.sleep(1) pub("127.0.0.1", 6379, CHANNEL_NAME)
Druhý skript je nepatrně složitější, protože se jedná o implementaci příjemce zprávy. Zde se již provádí větší množství operací, především registrace ke zvolenému kanálu:
r = connect(host, port) pubsub = r.pubsub() pubsub.subscribe(channel)
Následně jsou – zde velmi primitivním způsobem – zprávy čteny v programové smyčce metodou get_message(). Pokud se nevrátí None, je zpráva vypsána na obrazovku, jinak se klient snaží o další čtení z kanálu (což popravdě vypadá dosti neefektivně):
while True: print("Waiting for message published on " + channel) message = pubsub.get_message() if message: print("type {type} message '{message}'".format(type=message["type"], message=message["data"])) else: time.sleep(1) # lze snížit
Celá implementace příjemce zpráv může vypadat následovně:
import redis import time CHANNEL_NAME = "kanal1" def connect(host, port): return redis.Redis(host=host, port=port) def sub(host, port, channel): r = connect(host, port) pubsub = r.pubsub() pubsub.subscribe(channel) while True: print("Waiting for message published on " + channel) message = pubsub.get_message() if message: print("type {type} message '{message}'".format(type=message["type"], message=message["data"])) else: time.sleep(1) sub("127.0.0.1", 6379, CHANNEL_NAME)
Pokud nyní v samostatném terminálu spustíme skript nazvaný python_sub.py, měla by se na standardním výstupu nejprve objevit zpráva o úspěšném přihlášení do zvoleného kanálu. Tato zpráva může být schována mezi informativní řádky „Waiting for…“. Po přihlášení bude skript čekat na zprávy poslané do zvoleného kanálu, takže výstup vypsaný na terminál může vypadat například takto:
$ python3 python_sub.py Waiting for message published on kanal1 Waiting for message published on kanal1 type subscribe message '1' Waiting for message published on kanal1 Waiting for message published on kanal1 Waiting for message published on kanal1 ... ... ...
Nyní můžeme v dalším terminálu spustit druhý skript naprogramovaný v Pythonu, který bude zprávy (dokumenty) na kanál publikovat:
$ python3 python_pub.py Publishing message to kanal1 Publishing message to kanal1 Publishing message to kanal1 Publishing message to kanal1 Publishing message to kanal1 Publishing message to kanal1
Jakmile se předchozí skript spustí, začne první skript vypisovat informace o úspěšně přečtené zprávě. Tyto informace mohou být opět proloženy řádky „Waiting for…“:
type message message 'b'zprava #0'' Waiting for message published on kanal1 Waiting for message published on kanal1 type message message 'b'zprava #1'' Waiting for message published on kanal1 Waiting for message published on kanal1 type message message 'b'zprava #2'' Waiting for message published on kanal1 Waiting for message published on kanal1 type message message 'b'zprava #3''
Povšimněte si především toho, že běžné zprávy mají typ nastaven na message, zatímco první zpráva informující o připojení ke kanálu má typ subscribe.
Oba skripty ukončíme například s využitím klávesové zkratky Ctrl+C, popř. se můžeme pokusit se přihlásit do Redisu přímo z konzole a na ní sledovat komunikaci ve zvoleném kanálu (to již ostatně známe z předchozí kapitoly).
8. Naprogramování handleru pro čtení publikovaných zpráv
Předchozí „aktivní“ čekání na zprávy publikované na zvoleném kanálu nemusí být pro některé aplikace tím nejlepším řešením. Můžeme ovšem využít i handler, tj. (callback) funkci zavolanou ve chvíli, kdy je zpráva přijata. Zajímavé je, že ani v této chvíli se programové smyčky zpracovávající jednotlivé zprávy nezbavíme, což je vidět na následujícím příkladu upraveného klienta-příjemce zpráv. Samotné přihlášení k odběru zpráv z kanálu se změnilo tak, že metodě subscribe předáme keyword parametr pojmenovaný stejně jako kanál. Hodnotou pak bude reference na callback funkci (handler):
pubsub.subscribe(**{channel: handler})
Samotná implementace handleru je jednoduchá, protože se pouze vypíše typ zprávy a vlastní (textová) data zprávy:
def handler(message): print("type {type} message '{message}'".format(type=message["type"], message=message["data"]))
Smyčku pro detekci nových zpráv už musíme implementovat sami, ovšem s tím, že do větve if message se ve skutečnosti řízení dostane pouze jedenkrát, a to při přihlášení do kanálu:
while True: message = pubsub.get_message() if message: print(message) else: time.sleep(1)
Výsledný zdrojový kód upraveného příjemce zpráv na zvoleném kanálu může vypadat takto:
import redis import time CHANNEL_NAME = "kanal1" def connect(host, port): return redis.Redis(host=host, port=port) def handler(message): print("type {type} message '{message}'".format(type=message["type"], message=message["data"])) def sub(host, port, channel): r = connect(host, port) pubsub = r.pubsub() pubsub.subscribe(**{channel: handler}) while True: message = pubsub.get_message() if message: print(message) else: time.sleep(1) sub("127.0.0.1", 6379, CHANNEL_NAME)
Pokud nyní klienta spustíme a současně začneme do kanálu „kanal1“ posílat zprávy, klient si je přečte a vypíše jejich obsah:
$ python3 python_sub_handler.py {'type': 'subscribe', 'pattern': None, 'channel': b'kanal1', 'data': 1} type message message 'b'zprava #0'' type message message 'b'zprava #1'' type message message 'b'zprava #2'' type message message 'b'zprava #3'' type message message 'b'zprava #4'' type message message 'b'zprava #5'' type message message 'b'zprava #6''
9. Ignorování zpráv o přihlášení a odhlášení z kanálu
V některých případech nám může vadit implicitní chování Redisu při přihlášení k odebírání zpráv z nějakého kanálu. V tomto případě totiž odběratel dostane na začátku speciální zprávu o tom, že byl ke kanálu přihlášen:
{'type': 'subscribe', 'pattern': None, 'channel': b'kanal1', 'data': 1}
Řešení je jednoduché – postačuje metodě pubsub() předat pojmenovaný parametr ignore_subscribe_messages a nastavit ho na hodnotu True:
r = connect(host, port) pubsub = r.pubsub(ignore_subscribe_messages=True) pubsub.subscribe(channel)
Upravený příklad může vypadat následovně:
import redis import time CHANNEL_NAME = "kanal1" def connect(host, port): return redis.Redis(host=host, port=port) def sub(host, port, channel): r = connect(host, port) pubsub = r.pubsub(ignore_subscribe_messages=True) pubsub.subscribe(channel) while True: message = pubsub.get_message() if message: print("type {type} message '{message}'".format(type=message["type"], message=message["data"])) else: time.sleep(1) sub("127.0.0.1", 6379, CHANNEL_NAME)
Po spuštění tohoto příkladu se již první typ zprávy nikdy neobjeví:
$ python3 python_sub_ignore_noise.py type message message 'b'zprava #0'' type message message 'b'zprava #1'' type message message 'b'zprava #2'' type message message 'b'zprava #3'' type message message 'b'zprava #4''
10. Chování v případě, že je klient odebírající zprávy pomalý
V předchozích příkladech jsme se vlastně nezabývali situací, která je však poměrně běžná: příjemce zprávy (subscriber) nedokáže příchozí data zpracovat dostatečně rychle a je tedy pomalejší, než odesílatel zprávy (publisher). Toto chování si můžeme velmi snadno odsimulovat nepatrnou úpravou skriptů, s nimiž jsme se seznámili v předchozích kapitolách:
Skript, který publikuje zprávy s velkou rychlostí:
import redis import time CHANNEL_NAME = "kanal1" def connect(host, port): return redis.Redis(host=host, port=port) def pub(host, port, channel): r = connect(host, port) for i in range(0, 21): print("Publishing message to " + channel) message = "zprava #{}".format(i) r.publish(channel, message) time.sleep(0.001) pub("127.0.0.1", 6379, CHANNEL_NAME)
Skript, který sice zprávy odebírá, ale dokáže přečíst maximálně jednu zprávu za sekundu:
import redis import time CHANNEL_NAME = "kanal1" def connect(host, port): return redis.Redis(host=host, port=port) def sub(host, port, channel): r = connect(host, port) pubsub = r.pubsub(ignore_subscribe_messages=True) pubsub.subscribe(channel) while True: message = pubsub.get_message() if message: print("type {type} message '{message}'".format(type=message["type"], message=message["data"])) time.sleep(1) sub("127.0.0.1", 6379, CHANNEL_NAME)
Můžete se sami přesvědčit, že i v tomto případě bude Redis pracovat podle předpokladů tím nejlepším možným způsobem:
- Odesílatel své zprávy bez problému odpublikuje svou rychlostí a není nijak zdržován.
- Příjemce bude zprávy dostávat tak rychle, jak si sám určí, tj. jak rychle dokáže volat metodu get_message.
- V mezičase budou zprávy uloženy v paměti Redisu.
Ve skutečnosti si musíme dát pozor na to, že zprávy nebudou zachovány při restartu Redisu, tj. není zde implementováno stejné chování, jaké můžeme znát z klasických front zpráv. Pokud potřebujete zajistit chování fronty, může se použít nějaká knihovna postavená nad Redisem (těmito knihovnami se budeme zabývat příště).
11. Export dat do formátu CSV
V některých případech může být užitečné přečíst data z Redisu a uložit je do formátu CSV. Tuto operaci můžeme provést přímo pomocí nástroje redis-cli. Podívejme se na několik ukázek:
$ redis-cli smembers s1 1) "10" 2) "y" 3) "x" 4) "30" 5) "20" 6) "z" 7) "42" $ redis-cli --csv smembers s1 "10","y","x","30","20","z","42"
$ redis-cli lrange l -10 10 1) "10" 2) "20" 3) "30" $ redis-cli --csv lrange l -10 10 "10","20","30"
Výsledek si samozřejmě můžeme přesměrovat do souboru a případná chybová hlášení do odlišného souboru:
$ redis-cli --csv lrange l -10 10 > output.csv 2> errors.txt
Pro export většího množství záznamů je možné použít trik, který je popsán na adrese https://rdbtools.com/blog/redis-export-hashes-as-csv-using-cli/:
redis-cli --scan --pattern users:* |\ grep -e "^users:[^:]*$" |\ awk '{print "hmget " $0 " id display_name reputation location"}' |\ redis-cli --csv > users.csv
Výsledek prvního vyhledání (selectu) je zpracován interpretrem awk, který vygeneruje příkazy pro druhé spuštění nástroje redis-cli.
12. Protokol použitý pro komunikaci se serverem
Další zajímavou technologií, s nímž se můžete setkat, je samotný protokol použitý pro přenos dat mezi serverem Redisu a jednotlivými klienty, kteří se k serveru připojují. Tento protokol se jmenuje RESP neboli REdis Serialization Protocol. Protokol RESP byl navržen s ohledem na to, aby byl především:
- Jednoduchý na implementaci, a to jak na straně serveru, tak zejména klientů.
- Čitelný i pro člověka.
- Umožňující rychlý a efektivní parsing zpráv na straně klientů.
Se základním použitím tohoto protokolu (dotaz-odpověď) jsme se již seznámili a taktéž jsme si řekli, že existují tři výjimky, kdy se jednoduchý systém typu dotaz-odpověď nepoužívá. Jedná se o pipeline, o Pub/Sub kanály popsané výše a o streamy z Redisu 5.
Podívejme se nyní, jak vypadá standardní komunikace mezi serverem a klientem. Samotný dotaz je reprezentován jedním textovým řádkem, ovšem zajímavější je formát odpovědi. Redis totiž musí klientovi oznámit, jaký typ dat vlastně vrací. Typ odpovědi je jednoznačně určen prvním znakem – viz též následující tabulku:
První znak odpovědi | Význam |
---|---|
+ | vrací se jednoduchý řetězec, typicky nějaká zpráva (řetězec je jednořádkový) |
– | příkaz skončil z nějakého důvodu chybou, vrací se informace o chybě |
: | vrací se celé číslo |
$ | vrací se takzvaný Bulk String, což jsou ve skutečnosti binární data o maximální délce až 512 MB |
* | vrací se pole |
13. Příkazy posílané serveru
Příkazy, které jsou posílané serveru, lze reprezentovat dvěma způsoby:
- Jediný příkaz zapsaný formou řetězce a ukončeného znaky CR LF. Toto je ideální formát pro „ruční“ práci se serverem. V praxi si vystačíte s nástroji telnet (interaktivní práce) nebo ncat (dávkové zpracování).
- Druhý formát příkazu je založen na poli (array) obsahující jako své prvky takzvaný bulk stringy, jejichž formát je popsaný v sedmnácté kapitole. Tento formát je sice složitější, ovšem umožňuje, aby se serveru poslalo několik příkazů v jediném dotazu, což je samozřejmě rychlejší.
Pokud nechcete použít ncat a přitom si chcete vyzkoušet přímou komunikaci se serverem, můžete si naprogramovat jednoduchý skript, který se k serveru připojí a pošle mu data přes běžný socket:
from sys import argv, exit import time import socket BLOCK_LENGTH = 512 def connect(host, port): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((host, int(port))) return s def call_redis(host, port, command): s = connect(host, port) s.sendall(command.encode()) data = s.recv(BLOCK_LENGTH) if data: print(repr(data)) s.shutdown(socket.SHUT_WR) s.close() if __name__ == "__main__": if len(argv) < 4: print("usage: call_redis host port command") exit(1) cmd = " ".join(argv[3:]) + "\r\n" print(cmd) call_redis(argv[1], argv[2], cmd)
Zde je nutné poznamenat, že skript nedokáže pracovat s odpověďmi delšími než jeden blok (zde 512 bajtů, ovšem můžete si nastavit i jinou velikost).
Příklad použití:
$ python call_redis.py localhost 6379 publish kanal1 ahoj
14. Odpověď obsahující jednoduchý řetězec
Nyní si ve stručnosti popišme, jak vlastně vypadá odpověď serveru v případě, že výsledkem dotazu má být jednořádkový text. Jednoduchý řetězec vracený serverem může obsahovat prakticky libovolné znaky s výjimkou konce řádku. Konec řetězce je rozpoznán podle dvojice znaků CR LF neboli „\r\n“ (tato dvojice znaků, popř. jeden z těchto znaků, bude považován za konec řádku na prakticky všech v současnosti používaných operačních systémech, pokud ovšem používají ASCII :-). Příkladem odpovědi serveru zprávou reprezentovanou jednoduchým řetězcem může být:
"+OK\r\n"
Můžeme si to vyzkoušet, a to s využitím užitečného nástroje ncat [1], který lze použít namísto konzole Redisu. Všechny tři následující příkazy vrací odpověď ve formátu jednořádkového řetězce „+OK“:
$ echo "flushall" | ncat localhost 6379 +OK
$ echo "flushall async" | ncat localhost 6379 +OK
$ echo "memory purge" | ncat localhost 6379 +OK
15. Odpověď vrácená serverem v případě chyby
V případě, že nějaký příkaz skončil s chybou, opět se vrací řetězec, ovšem jeho prvním znakem nyní bude „-“ a nikoli „+“. Klient, který zprávy zpracovává, tedy může použít nějakou jednoduchou formu řídicí konstrukce if/switch pro rozlišení obou sémanticky odlišných výsledků:
"-Chybova zprava\r\n"
Příklad reálných chybových hlášení, které Redis skutečně může posílat:
"-ERR unknown command 'cmd'" "-WRONGTYPE Operation against a key holding the wrong kind of value"
Opět si to samozřejmě můžeme otestovat pomocí nástroje ncat:
$ echo "Ereš pikloš neméšči huňár scépeň kámoš" | ncat localhost 6379 -ERR unknown command 'Ereš'
Prvek nazvaný „x“ obsahuje řetězec, tj. nejedná se o množinu:
$ echo "smembers x" | ncat localhost 6379 -WRONGTYPE Operation against a key holding the wrong kind of value
Ve skutečnosti však mnoho příkazů nekončí chybovým hlášením, ale prostým zasláním celočíselné hodnoty typu 0 nebo 1, což jsme ostatně mohli vidět už v předchozím článku.
16. Odpověď obsahující celé číslo (integer)
Dalším typem odpovědi serveru klientovi je zpráva obsahující celé číslo. Může se jednat o délku seznamu, počet klientů naslouchajících na nějakém kanálu atd. Samotná celočíselná hodnota je přenesena v textové podobě, takže jediný rozdíl oproti jednořádkovému řetězci nebo chybové zprávě představuje první znak, který u celočíselné odpovědi musí obsahovat dvojtečku. Opět si ukažme příklad, jak může odpověď serveru vypadat:
":42\r\n"
Několik příkladů z praxe (příkazy incr a llen jsme si popsali minule):
$ echo "incr citac" | ncat localhost 6379 :1 $ echo "incr citac" | ncat localhost 6379 :2 $ echo "llen seznam1" | ncat localhost 6379 :4 $ echo "llen seznamX" | ncat localhost 6379 :0
Ve skutečnosti se však hodnoty čítačů vracejí formou řetězce! Tj. je velký rozdíl mezi návratovou hodnotou příkazu incr citac a návratovou hodnotou get citac. Opět se podívejme na příklady, kde pro zjednodušení budeme současně pracovat v konzoli Redisu i s příkazem ncat.
Konzole Redisu:
127.0.0.1:6379 >set y 0 OK 127.0.0.1:6379 >INCRBY y 1 (integer) 1 127.0.0.1:6379 >get y "1"
Příkazový řádek:
$ echo "get y" | ncat localhost 6379 $1 1
Konzole Redisu:
127.0.0.1:6379 >set x 0 OK 127.0.0.1:6379 >INCRBYFLOAT x 0.5 "0.5" 127.0.0.1:6379 >get x "0.5"
Příkazový řádek:
$ echo "get x" | ncat localhost 6379 $3 0.5
17. Odpověď s dlouhým řetězcem resp. blokem bajtů
Krátké řetězce, které byly popsány v kapitolách 14 a 15, se hodí především pro vrácení kratších textů, například informací o provedení či naopak o neprovedení příkazu atd. Systém Redis se však používá mj. i pro ukládání rozsáhlejších binárních dat. Tato data se vrací ve formě takzvaného bulk stringu, což však může být poněkud matoucí označení, protože se ve skutečnosti jedná o binární blok o maximální délce až 512 MB. Samotnou interpretaci tohoto binárního bloku ponechává systém Redis na klientovi (což se může týkat například použití UTF-8 atd.). Tento typ odpovědi začíná znakem „$“. Za tímto znakem je uvedena celková délka řetězce oddělená od zbytku odpovědi nám již známou dvojicí znaků CR LF. Délka v tomto případě reprezentuje počet bajtů, nikoli počet znaků. Následuje sekvence jednotlivých bajtů, která je ukončena znaky CR LF (ty jsou ve skutečnosti nadbytečné, neboť si klient může sám programově hlídat počet průběžně načítaných bajtů).
"$4\r\ntest\r\n"
V případě, že je zapotřebí reprezentovat prázdný řetězec resp. prázdný blok (což může být poměrně častý požadavek), pošle server následující sekvenci bajtů:
"$0\r\n\r\n"
Tento typ odpovědi navíc může být použit v těch případech, kdy server vrací hodnotu s významem Null (žádná požadovaná hodnota neexistuje). Taková odpověď je reprezentována řetězcem, jehož délka je rovna –1. Povšimněte si, že takový řetězec se od prázdného řetězce odlišuje mj. i v tom, že neobsahuje druhou dvojici znaků CR LF:
"$-1\r\n"
Podívejme se na příklad, kdy server vrací bulk string:
$ echo "client list" | ncat localhost 6379 $148 id=14 addr=127.0.0.1:54950 fd=9 name= age=0 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client
Příklad, kdy server vrátí hodnotu odpovídající Null:
$ echo "get foobarbaz" | ncat localhost 6379 $-1
18. Odpověď obsahující pole hodnot
Nejsložitější je poslední typ odpovědi, kterou server vrací pole hodnot. Tato odpověď začíná znakem „*“, za nímž následuje počet prvků pole, který je opět ukončen dvojicí znaků CR LF. Za těmito znaky následují jednotlivé prvky pole zakódované způsobem popsaným v předchozích kapitolách (řetězec, celočíselná hodnota, bulk string atd.), vždy s uvedením typu.
Nejjednodušší je samozřejmě situace, kdy server vrací prázdné pole, které má nulový počet prvků. Takové pole je serverem vráceno ve formátu:
"*0\r\n"
Příklady z praxe pro prázdné množiny:
$ echo "smembers s0" | ncat localhost 6379 *0 $ echo "smembers s2" | ncat localhost 6379 *0
Ukažme si pro ilustraci ještě další typy polí. V případě, že se má vrátit pole s několika řetězci typu Bulk String (zde konkrétně se třemi řetězci), pošle server klientovi následující odpověď:
"*3\r\n$5\r\nprvni\r\n$5\r\ndruhy\r\n$5\r\ntreti\r\n"
Neboli v čitelnější podobě po přepisu znaků CR LF za konec řádku:
*3 $5 prvni $5 druhy $5 treti
Opět příklad z praxe pro množinu se dvěma prvky:
127.0.0.1:6379 > $ echo "smembers s1" | ncat localhost 6379 *2 $2 42 $3 100
Server samozřejmě může vrátit i pole celých čísel, a to v podobě:
"*3\r\n:1\r\n:2\r\n:3\r\n"
Opět si můžeme vyzkoušet přepis do čitelnější podoby po přepisu znaků CR LF za konec řádku:
*3 :10 :20 :30
*6 :10 $5 druhy :20 $6 ctvrty +OK :0
Výsledek sice již není příliš čitelný, ovšem samotný parser může zůstat velmi jednoduchý a především rychlý.
Příklad – získání informací o kanálech a odebíratelech kanálů:
$ echo "pubsub numsub kanal1 kanal2 kanal3" | ncat localhost 6379 *6 $6 kanal1 :1 $6 kanal2 :0 $6 kanal3 :0
Vrácení seznamu řetězců:
$ echo "lrange seznam1 0 1000" | ncat localhost 6379 *4 $5 prvni $5 druhy $5 treti $6 ctvrty
19. Repositář s demonstračními příklady
Zdrojové kódy všech dnes popsaných demonstračních příkladů naprogramovaných v Pythonu byly uloženy do Git repositáře, který je dostupný na adrese https://github.com/tisnik/py-redis-examples (stále na GitHubu :-). V případě, že nebudete chtít klonovat celý repositář (ten je ovšem – alespoň prozatím – velmi malý, dnes má doslova několik kilobajtů), můžete namísto toho použít odkazy na jednotlivé příklady, které naleznete v následující tabulce:
# | Demonstrační příklad | Popis | Cesta |
---|---|---|---|
1 | call_redis.py | použití socketů při komunikaci se serverem Redisu | https://github.com/tisnik/py-redis-examples/blob/master/call_redis.py |
2 | python_pub.py | použití paradigmatu publish-subscibe, část pro publikování zpráv | https://github.com/tisnik/py-redis-examples/blob/master/python_pub.py |
3 | python_sub.py | použití paradigmatu publish-subscibe, část pro příjem zpráv | https://github.com/tisnik/py-redis-examples/blob/master/python_sub.py |
4 | python_sub_handler.py | handler zpracovávající přijaté zprávy | https://github.com/tisnik/py-redis-examples/blob/master/python_sub_handler.py |
5 | python_sub_ignore_noise.py | ignorování zpráv s informací o připojení ke kanálu atd. | https://github.com/tisnik/py-redis-examples/blob/master/python_sub_ignore_noise.py |
6 | python_pub_faster.py | publisher, který publikuje zprávy rychleji, než příjemce | https://github.com/tisnik/py-redis-examples/blob/master/python_pub_faster.py |
7 | python_sub_slow_read.py | pomalý příjemce zpráv | https://github.com/tisnik/py-redis-examples/blob/master/python_sub_slow_read.py |
20. Odkazy na Internetu
- Stránky projektu Redis
https://redis.io/ - Introduction to Redis
https://redis.io/topics/introduction - Try Redis
http://try.redis.io/ - Redis tutorial, April 2010 (starší, ale pěkně udělaný)
https://static.simonwillison.net/static/2010/redis-tutorial/ - Python Redis
https://redislabs.com/lp/python-redis/ - Redis: key-value databáze v paměti i na disku
https://www.zdrojak.cz/clanky/redis-key-value-databaze-v-pameti-i-na-disku/ - Praktický úvod do Redis (1): vaše distribuovaná NoSQL cache
http://www.cloudsvet.cz/?p=253 - Praktický úvod do Redis (2): transakce
http://www.cloudsvet.cz/?p=256 - Praktický úvod do Redis (3): cluster
http://www.cloudsvet.cz/?p=258 - Connection pool
https://en.wikipedia.org/wiki/Connection_pool - Instant Redis Sentinel Setup
https://github.com/ServiceStack/redis-config - How to install REDIS in LInux
https://linuxtechlab.com/how-install-redis-server-linux/ - Redis RDB Dump File Format
https://github.com/sripathikrishnan/redis-rdb-tools/wiki/Redis-RDB-Dump-File-Format - Lempel–Ziv–Welch
https://en.wikipedia.org/wiki/Lempel%E2%80%93Ziv%E2%80%93Welch - Redis Persistence
https://redis.io/topics/persistence - Redis persistence demystified
http://oldblog.antirez.com/post/redis-persistence-demystified.html - Redis reliable queues with Lua scripting
http://oldblog.antirez.com/post/250 - Ost (knihovna)
https://github.com/soveran/ost - NoSQL
https://en.wikipedia.org/wiki/NoSQL - Shard (database architecture)
https://en.wikipedia.org/wiki/Shard_%28database_architecture%29 - What is sharding and why is it important?
https://stackoverflow.com/questions/992988/what-is-sharding-and-why-is-it-important - What Is Sharding?
https://btcmanager.com/what-sharding/ - Redis clients
https://redis.io/clients - Category:Lua-scriptable software
https://en.wikipedia.org/wiki/Category:Lua-scriptable_software - Seriál Programovací jazyk Lua
https://www.root.cz/serialy/programovaci-jazyk-lua/ - Redis memory usage
http://nosql.mypopescu.com/post/1010844204/redis-memory-usage - Ukázka konfigurace Redisu pro lokální testování
https://github.com/tisnik/presentations/blob/master/redis/redis.conf - Resque
https://github.com/resque/resque - Nested transaction
https://en.wikipedia.org/wiki/Nested_transaction - Publish–subscribe pattern
https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern - Messaging pattern
https://en.wikipedia.org/wiki/Messaging_pattern - Using pipelining to speedup Redis queries
https://redis.io/topics/pipelining - Pub/Sub
https://redis.io/topics/pubsub - ZeroMQ distributed messaging
http://zeromq.org/ - Publish/Subscribe paradigm: Why must message classes not know about their subscribers?
https://stackoverflow.com/questions/2908872/publish-subscribe-paradigm-why-must-message-classes-not-know-about-their-subscr - Python & Redis PUB/SUB
https://medium.com/@johngrant/python-redis-pub-sub-6e26b483b3f7 - Message broker
https://en.wikipedia.org/wiki/Message_broker - RESP Arrays
https://redis.io/topics/protocol#array-reply - Redis Protocol specification
https://redis.io/topics/protocol - Redis Pub/Sub: Intro Guide
https://www.redisgreen.net/blog/pubsub-intro/ - Redis Pub/Sub: Howto Guide
https://www.redisgreen.net/blog/pubsub-howto/ - Comparing Publish-Subscribe Messaging and Message Queuing
https://dzone.com/articles/comparing-publish-subscribe-messaging-and-message - ActiveMQ
http://activemq.apache.org/activemq-website/index.html - Amazon Simple Queue Service
https://aws.amazon.com/sqs/ - Apache Kafka
https://kafka.apache.org/ - Cloud Pub/Sub
https://cloud.google.com/pubsub/ - Introduction to Redis Streams
https://redis.io/topics/streams-intro - glob (programming)
https://en.wikipedia.org/wiki/Glob_(programming)