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
Mateusz M.

Ostatnie wpisy

Podsumowanie: maj, czerwiec, lipiec i sierpień 2024

Oj daaawnoo mnie tu nie było. Ale wakacje to był czas dużej liczby intensywnych wyjazdów i tak naprawdę, dopiero jakoś… Read More

2 miesiące ago

Podsumowanie: kwiecień 2024

Cześć! Zapraszam na krótkie podsumowanie kwietnia. Wyjazd do Niemiec A dokładniej pod granicę z Francją. Chrześnica miała pierwszą komunię. Po… Read More

6 miesięcy ago

Podsumowanie: luty i marzec 2024

Ostatnio tygodnie były tak bardzo wypełnione, że nie udało mi się napisać nawet krótkiego podsumowanie. Więc dziś zbiorczo podsumuję luty… Read More

7 miesięcy ago

Podsumowanie: styczeń 2024

Zapraszam na krótkie podsumowanie miesiąca. Książki W styczniu przeczytałem "Homo Deus: Historia jutra". Książka łudząco podoba do wcześniejszej książki tego… Read More

9 miesięcy ago

Podsumowanie roku 2023

Cześć! Zapraszam na podsumowanie roku 2023. Książki Zacznijmy od książek. W tym roku cel 35 książek nie został osiągnięty. Niemniej… Read More

10 miesięcy ago

Podsumowanie: grudzień 2023

Zapraszam na krótkie podsumowanie miesiąca. Książki W grudniu skończyłem czytać Mein Kampf. Nudna książka. Ciekawsze fragmenty można by było streścić… Read More

11 miesięcy ago