Hlavní navigace

Databáze Redis (nejenom) pro vývojáře používající Python (dokončení)

22. 11. 2018
Doba čtení: 28 minut

Sdílet

 Autor: Redis
Ve druhé části článku o projektu Redis si ukážeme další možnosti, které nám tento nástroj nabízí. Zaměříme se přitom jak na přímé použití konzole redis-cli, tak i na volání funkcí Redisu z Pythonu.

Obsah

1. Databáze Redis (nejenom) pro vývojáře používající Python (dokončení)

2. Odpovědi serveru vracející pole (array)

3. Hodnoty nil

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ý

11. Export dat do formátu CSV

12. Protokol použitý pro komunikaci se serverem

13. Příkazy posílané serveru

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

20. Odkazy na Internetu

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
Poznámka: jen pro připomenutí – do seznamů lze prvky přidávat jak na konec, tak vkládat na začátek. Podobně je možné ze seznamů prvky vybírat ze začátku i z konce, takže tato struktura je v praxi využitelná i jako zásobník (stack), fronta (queue) či obousměrná fronta (deque). Pravděpodobně nejčastěji se setkáme s použitím fronty.

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:

  1. Existuje jeden či několik zdrojů zpráv.
  2. Příjemců může být taktéž více, ovšem zpráva je typicky získána jen jedním z nich.
  3. Obecně není zaručeno pořadí doručení zpráv.
  4. 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.
  5. 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:

  1. Existuje jeden či několik zdrojů zpráv.
  2. 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.
  3. Pořadí zpráv je zaručeno.
  4. 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/wi­ki/Glob_(programming).

Poznámka: protokol používaný Redisem je většinou založen na té nejjednodušší možné komunikaci typu dotaz-odpověď. To znamená, že každý příkaz poslaný klientem na server je následován odpovědí serveru zpět klientovi. Typicky jsou buď klientovi poslána data nebo alespoň celočíselná hodnota 0 nebo 1 reprezentující úspěch popř. neúspěch příkazu. Existují však tři výjimky, kdy se dotaz-odpověď nepoužívá. První výjimkou jsou takzvané pipeline, kdy klient zasílá více příkazů v jednom balíčku. Druhou výjimkou je právě použití Pub/Sub kanálů, protože v této chvíli se začne používat push protokol – server sám začíná posílat zprávy ve chvíli, kdy jsou publikovány nějakým jiným klientem. Třetí výjimka se objevila v páté verzi Redisu a souvisí se streamy a příkazem XREAD. Popisem streamů se však dnes zabývat nebudeme.

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ů:

  1. V prvním terminálu bude spuštěn redis-server. Můžeme zde povolit logování.
  2. Ve druhém terminálu spustíme první konzoli Redisu.
  3. 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
Poznámka: skutečně prosím použijte zmíněný konfigurační soubor nebo nějakou jeho obdobu. Budete tak mít jistotu, že server Redisu bude naslouchat pouze na lokálním rozhraní 127.0.0.1 a nebude tak omylem „otevřený“ do celého Internetu.

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“.

Poznámka: odpověď serveru má formát pole, čímž se vracíme k důvodu, proč byla napsána druhá kapitola :-)

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

Poznámka: povšimněte si, že se v tomto případě vrátí pole, kde liché prvky obsahují jména kanálů a sudé prvky počty odebíratelů:

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})
Poznámka: tuto syntaxi volání musíme použít z toho důvodu, že jméno kanálu je uloženo v proměnné a není tedy reprezentováno přímo konstantou, která by nám umožnila jednodušší zápis.

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''
Poznámka: opět si povšimněte, že první zprávou, kterou dostaneme, je informace o úspěšném přihlášení ke kanálu. Je to současně jediná zpráva, která není zpracována handlerem.

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:

  1. Odesílatel své zprávy bez problému odpublikuje svou rychlostí a není nijak zdržován.
  2. Příjemce bude zprávy dostávat tak rychle, jak si sám určí, tj. jak rychle dokáže volat metodu get_message.
  3. 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:

  1. Jednoduchý na implementaci, a to jak na straně serveru, tak zejména klientů.
  2. Čitelný i pro člověka.
  3. 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:

  1. 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í).
  2. 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"
Poznámka: uvozovky nejsou součástí řetězce.

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"
Poznámka: tento způsob posílání dat je velmi praktický, protože klient již dopředu ví, jak velký paměťový blok si bude muset naalokovat. Navíc blok může obsahovat libovolné hodnoty bajtů, včetně bajtu nulového.

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
Poznámka: znaky CR LF nyní byly interpretovány textovou konzolí/terminálem, takže uvidíme hodnotu $148 s délkou řetězce na prvním řádku odpovědi a vlastní řetězec na řádku druhém (popř. na řádcích následujících).

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
Poznámka: v této kapitole sice, stejně jako je tomu v oficiální dokumentaci Redisu, používáme termín „pole“, ovšem v Redisu se pole spíše podobají klasickým seznamům, protože se nemusí jednat o homogenní datový typ. Pokud by například pole obsahovalo prvky typu řetězec, celé číslo a bulk string, je to zcela legitimní případ a vypadal by následovně:
*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ů:

CS24_early

$ 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_han­dler.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_ig­nore_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_fas­ter.py
7 python_sub_slow_read.py pomalý příjemce zpráv https://github.com/tisnik/py-redis-examples/blob/master/python_sub_slow_re­ad.py

20. Odkazy na Internetu

  1. Stránky projektu Redis
    https://redis.io/
  2. Introduction to Redis
    https://redis.io/topics/introduction
  3. Try Redis
    http://try.redis.io/
  4. Redis tutorial, April 2010 (starší, ale pěkně udělaný)
    https://static.simonwilli­son.net/static/2010/redis-tutorial/
  5. Python Redis
    https://redislabs.com/lp/python-redis/
  6. 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/
  7. Praktický úvod do Redis (1): vaše distribuovaná NoSQL cache
    http://www.cloudsvet.cz/?p=253
  8. Praktický úvod do Redis (2): transakce
    http://www.cloudsvet.cz/?p=256
  9. Praktický úvod do Redis (3): cluster
    http://www.cloudsvet.cz/?p=258
  10. Connection pool
    https://en.wikipedia.org/wi­ki/Connection_pool
  11. Instant Redis Sentinel Setup
    https://github.com/ServiceStack/redis-config
  12. How to install REDIS in LInux
    https://linuxtechlab.com/how-install-redis-server-linux/
  13. Redis RDB Dump File Format
    https://github.com/sripat­hikrishnan/redis-rdb-tools/wiki/Redis-RDB-Dump-File-Format
  14. Lempel–Ziv–Welch
    https://en.wikipedia.org/wi­ki/Lempel%E2%80%93Ziv%E2%80%93­Welch
  15. Redis Persistence
    https://redis.io/topics/persistence
  16. Redis persistence demystified
    http://oldblog.antirez.com/post/redis-persistence-demystified.html
  17. Redis reliable queues with Lua scripting
    http://oldblog.antirez.com/post/250
  18. Ost (knihovna)
    https://github.com/soveran/ost
  19. NoSQL
    https://en.wikipedia.org/wiki/NoSQL
  20. Shard (database architecture)
    https://en.wikipedia.org/wi­ki/Shard_%28database_archi­tecture%29
  21. What is sharding and why is it important?
    https://stackoverflow.com/qu­estions/992988/what-is-sharding-and-why-is-it-important
  22. What Is Sharding?
    https://btcmanager.com/what-sharding/
  23. Redis clients
    https://redis.io/clients
  24. Category:Lua-scriptable software
    https://en.wikipedia.org/wi­ki/Category:Lua-scriptable_software
  25. Seriál Programovací jazyk Lua
    https://www.root.cz/seria­ly/programovaci-jazyk-lua/
  26. Redis memory usage
    http://nosql.mypopescu.com/pos­t/1010844204/redis-memory-usage
  27. Ukázka konfigurace Redisu pro lokální testování
    https://github.com/tisnik/pre­sentations/blob/master/re­dis/redis.conf
  28. Resque
    https://github.com/resque/resque
  29. Nested transaction
    https://en.wikipedia.org/wi­ki/Nested_transaction
  30. Publish–subscribe pattern
    https://en.wikipedia.org/wi­ki/Publish%E2%80%93subscri­be_pattern
  31. Messaging pattern
    https://en.wikipedia.org/wi­ki/Messaging_pattern
  32. Using pipelining to speedup Redis queries
    https://redis.io/topics/pipelining
  33. Pub/Sub
    https://redis.io/topics/pubsub
  34. ZeroMQ distributed messaging
    http://zeromq.org/
  35. Publish/Subscribe paradigm: Why must message classes not know about their subscribers?
    https://stackoverflow.com/qu­estions/2908872/publish-subscribe-paradigm-why-must-message-classes-not-know-about-their-subscr
  36. Python & Redis PUB/SUB
    https://medium.com/@johngrant/python-redis-pub-sub-6e26b483b3f7
  37. Message broker
    https://en.wikipedia.org/wi­ki/Message_broker
  38. RESP Arrays
    https://redis.io/topics/protocol#array-reply
  39. Redis Protocol specification
    https://redis.io/topics/protocol
  40. Redis Pub/Sub: Intro Guide
    https://www.redisgreen.net/blog/pubsub-intro/
  41. Redis Pub/Sub: Howto Guide
    https://www.redisgreen.net/blog/pubsub-howto/
  42. Comparing Publish-Subscribe Messaging and Message Queuing
    https://dzone.com/articles/comparing-publish-subscribe-messaging-and-message
  43. ActiveMQ
    http://activemq.apache.org/activemq-website/index.html
  44. Amazon Simple Queue Service
    https://aws.amazon.com/sqs/
  45. Apache Kafka
    https://kafka.apache.org/
  46. Cloud Pub/Sub
    https://cloud.google.com/pubsub/
  47. Introduction to Redis Streams
    https://redis.io/topics/streams-intro
  48. glob (programming)
    https://en.wikipedia.org/wi­ki/Glob_(programming)

Byl pro vás článek přínosný?

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.