Mateusz Mazurek – programista z pasją

Czyli o użyciu Pythona i kilku innych technologii do tworzenia świetnej jakości aplikacji w oparciu o stabilny proces dostarczania oprogramowania.

Algorytmika Programowanie Programowanie webowe

Redis i Python – dobrze dobrana para #7

Cześć.

Miałem chwilę przerwy od tej serii, ale już wracam. Poprzednie wpisy to solidna dawka podstawowej wiedzy na temat Redisa i jego użycia z poziomu Pythona. Tak po prawdzie, to można się pokusić o stwierdzenie, że ostatnio opublikowany artykuł wykracza ponad poziom podstawowy. Nie oznacza to jednak że kończmy. Powiem więcej – jeszcze trochę przed nami. Seria artykułów o Pythonie i Redisie to już 6 wpisów:

Więc jeśli któryś pominąłeś to sugeruję nadrobić.

Zaczynajmy.

Pipelining

Czyli „przetwarzanie potokowe”. Sytuacja kiedy mamy do wykonania na kluczach jakąś sekwencję operacji nie jest sytuacją rzadką. Do komunikacji z Redisem wykorzystywany jest protokół TCP, zapewnia on niezawodność w dostarczaniu danych i ewentualne retransmisje. Jest dobrym wyborem twórców. Warto pamiętać jednak że przesyłanie danych przez sieć zawsze trwa. Szczególnie właśnie w sytuacji, kiedy wykonujemy kilka (tysięcy) operacji następujących po sobie. Aby skrócić RTT (Round Trip Time) Redis udostępnia mechanizm pipeling. Pozwala on wysłać wiele komend do Redisa „za jednym razem” – oszczędzamy tym samym czas którego potrzebujemy żeby przesłać dane przez sieć.

Zerknij na poniższy „benchmark”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from timeit import timeit

setup = """from redis import Redis
from timeit import timeit

redis_connection = Redis(decode_responses=True, db=0)

key = "test"

redis_connection.set(key, 0)
"""


stmt1 = """
i = 10000
while i >= 0:
    redis_connection.incr(key)
    i -= 1

"""


stmt2 = """
i = 10000
with redis_connection.pipeline() as pipe:
    while i >= 0:
        pipe.incr(key)
        i -= 1
    pipe.execute()
"""


print(timeit(stmt1, setup=setup, number=10))
print(timeit(stmt2, setup=setup, number=10))

Prezentuje on dwa podejścia, pierwsze, pokazane w stmt1 to podejście naiwne, a stmt2 to podejście używające pipeling’u. Efekt wykonania się tego programu jest niedeterministyczny, ale myślę że sam rząd wielkości już sporo powie:

stmt1: 9.824363377003465
stmt2: 1.3603699850209523

To co trzeba tu koniecznie zapamiętać to zagrożenie wysycenia pamięci RAM. Redis w momencie pipe.execute() zwraca rezultat każdej operacji, więc oczywistym jest że owe wyniki wszystkich komend musi sobie kolejkować w pamięci. Sugestia ode mnie jest jedna: z głową dobieraj maksymalną ilość komend uruchamianych pod pipeline’em.

Dodatkowym atutem tego podejścia jest to, że przekłada się na wzrost wydajności samego serwera Redisa. Wynika to oczywiście z faktu że dla Redisa koszt wyciągnięcia jakieś danej z klucza jest niski, ale odesłanie jej via TCP – już niekoniecznie. Pipeline sprawia, że Redis odpowiada „raz, a dobrze”.

Transakcje

Transakcje to jedno z podstawowych pojęć współczesnych systemów baz danych. Umożliwiają one współbieżny dostęp do zawartości bazy danych.

Istotą transakcji jest integrowanie kilku operacji w jedną niepodzielną całość.

Temat ten pojawiał się w tym wpisie nie przez przypadek. Wspomniane wcześniej pipeline’y idealnie się łączą z ideą transakcyjności. Co więcej, kawałek kodu który pokazałem wyżej jest w pełni transakcyjny. Metoda pipeline, która zwraca context managera może przyjąć dodatkowy parametr o nazwie transaction mający domyślną wartość ustawioną na True.

Transakcje w Redisie opierają się o kilka komend: MULTI, EXEC, DISCARD i WATCH i zapewniają, że:

  • komendy pod transakcją wykonują się sekwencyjnie i nie ma możliwości by taki ciąg komend został przerwany przez innego klienta tego samego serwera,
  • transakcje zapewniają podejście „wszystko albo nic”, czyli ciąg komend pod transakcją jest atomowy. Albo wszystkie komendy zostaną wykonane albo żadna (zakładając brak błędów, o czym później)

Komenda MULTI rozpoczyna transakcję. Wszystko po niej jest objęte ową transakcją której wykonanie rozpoczyna się wraz z wykonaniem EXEC. Zwraca ona rezultat wykonanych komend. Aby anulować transakcję można użyć komendy DISCARD.

Transakcja może się nie udać z dwóch powodów:

  • przed wykonaniem EXEC np. składnia komendy jest niepoprawna
  • po wykonaniu EXEC np. efekt wykonania się komendy nie jest poprawny (np. komenda niedopasowana do typu)

W pierwszym przypadku cała transakcja zostanie odrzucona, w drugim – zostanie wykonana ta część transakcji która się udała. Zachowanie w drugim przypadku nie jest spójne z bazami SQL, gdzie taka transakcja zostałaby odrzucona. Tłumaczenie się twórców w tym kontekście nie do końca mnie przekonuje.

Jest jeszcze jedna komenda o której nie można nie wspomnieć, poruszając wątek transakcji, a jest nią WATCH. Pozwala ona na detekcję tego, czy klucze nie zmieniły się przypadkiem od czasu kiedy zaczęliśmy je obserwować. Jeśli się zmieniły to komenda EXEC zakończy się błędem:

1
2
3
4
5
6
7
8
9
10
11
12
127.0.0.1:6379> WATCH key
OK

// tutaj zmieniamy wartość klucza "key" z poziomu innego klienta

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> GET key
QUEUED
127.0.0.1:6379> EXEC
(nil) // informacja o niepowodzeniu
127.0.0.1:6379>

Pipeline i transakcje

Parametr transaction o którym wspomniałem wcześniej nie robi nic nadzwyczajnego – dodaje on po prostu komendy które powołują do życia transakcję, czyli MULTI, EXEC itp. Jest do standardowe użycie pipeline i transakcji – zapewniamy że raz wysłana paczka komend wykona się atomowo. Spójność tego rozwiązania jest piękna.

Podsumowując

Przeszliśmy sobie przez temat pipeline’ów i transakcji. Wyjaśniliśmy gdzie jest zysk z używaniach tego pierwszego mechanizmu i czego spodziewać się po drugim. Oba rozwiązania używane razem, tak jak to sugeruje biblioteka, dostarczają spójny mechanizm pozwalający na atomowa paczkowanie komend.

Dzięki za wizytę,
Mateusz Mazurek

A może wolisz nowości na mail?

Subskrybuj
Powiadom o
guest

Witryna wykorzystuje Akismet, aby ograniczyć spam. Dowiedz się więcej jak przetwarzane są dane komentarzy.