Cześć.
Tak jak pisałem w poprzednim artykule z tej serii, podstawowe zagadnienia związane z Redisem i jego użyciem z poziomu Pythona mamy za sobą. Ten wpis, jak i dwa ostatnie, to już tematy które przekraczają magiczną barierę podpisaną „podstawy” wgłębiając się w nieco bardziej zaawansowane zagadnienia. Dzisiejszy blog post będzie traktował o skryptach które można napisać w języku Lua. Takie skrypty można wysłać jako komendę do Redisa i wykonać. To tak w dużym skrócie. Ciekawe? No to zapraszam na dalszą część wpisu. Ale, ale! Nim przejdziesz dalej, upewnij się że nic Ci nie uciekło po drodze, bo to już 8ma część serii!
- https://mmazurek.dev/redis-i-python-dobrze-dobrana-para-7/
- https://mmazurek.dev/redis-i-python-dobrze-dobrana-para-6/
- https://mmazurek.dev/redis-i-python-dobrze-dobrana-para-5/
- https://mmazurek.dev/redis-i-python-dobrze-dobrana-para-4/
- https://mmazurek.dev/redis-i-python-dobrze-dobrana-para-3/
- https://mmazurek.dev/redis-i-python-dobrze-dobrana-para-2/
- https://mmazurek.dev/redis-i-python-dobrze-dobrana-para-1/
Czym jest Lua?
Lua to skryptowy język programowania. Pierwotnie projektowany był w celu rozszerzenia funkcjonalności różnych aplikacji, jednak jest często używany jako samodzielny język. Składnia tego języka jest prosta i przypomina składnię większości języków programowania. W odróżnieniu od Pythona, Lua jest daleki od podejścia „batteries included”, co sprawia, że pełny interpreter tego języka to zaledwie 247 kB. A to natomiast powoduje, że w pełni pasuje do tego do czego był tworzony – do rozszerzania możliwości innych aplikacji. I dokładnie w takim kontekście wykorzystuje go Redis.
Przykład
Bo przykład powie więcej niż 1000 słów.
1 2 3 4 5 6 7 8 9 10 | from redis import Redis redis_connection = Redis(decode_responses=True, db=0) script = """ return "test" """ print(redis_connection.eval(script, 0)) |
Powyższy kod wykonuję komendę EVAL, przekazując w niej po prostu ciało skryptu napisanego w LUA, Redis odbiera ten kod, wykonuje go i zwraca rezultat. Efektem wykonania się tego kodu jest wyświetlenie słowa „test”.
Drugi argument w poleceniu EVAL to 0 i określa on ilość argumentów które można przekazać do skryptu. Jeśli podamy tam wartość „n” to kolejne „n” argumentów które przekażemy do funkcji będą przekazane również do skryptu. W skrypcie będą one dostępne w tabeli KEYS (uwaga – Lua indeksuje tabele od 1!). Wszystko przekazane po określonej liczbie „n”, zostanie przekazane do tabeli ARGV.
Zerknij na kod poniżej:
1 2 3 4 5 6 7 8 9 10 | from redis import Redis redis_connection = Redis(decode_responses=True, db=0) script = """ return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]} """ print(redis_connection.eval(script, 2, 1, 2, 'first', 'second')) |
Określamy tutaj że dwa pierwsze parametry będą dostępne w skrypcie w tabeli KEYS a kolejne – w tabeli ARGV. Sam skrypt odczytuje przekazane dane i zwraca je jako tablicę. Wynik tego kawałka kodu to oczywiście:
['1', '2', 'first', 'second']
Oczywiście Lua umie wszystko co inne języki programowania, więc nic nie stoi na przeszkodzie by np. wygenerować sobie w nim listę 10 liczb:
1 2 3 4 5 6 7 8 9 10 11 12 13 | from redis import Redis redis_connection = Redis(decode_responses=True, db=0) script = """ local arr = {} for i = 0, 10 do arr[i] = i end return arr """ print(redis_connection.eval(script, 0))# lista od 1 do 10 |
albo pracować z formatem danych JSON:
1 2 3 4 5 6 7 8 9 10 11 12 | from redis import Redis from json import dumps redis_connection = Redis(decode_responses=True, db=0) script = """ local json_data = KEYS[1] local decoded_data = cjson.decode(json_data) return decoded_data['a'] + decoded_data['b'] """ print(redis_connection.eval(script, 1, dumps({'a': 1, 'b': 6}))) # wynik to 7 |
No dobra, ale po co to?
Skrypty w Redisie pojawiły się po to by rozwiązać problem sytuacji kiedy potrzebujemy coś pobrać z Redisa, przetworzyć i zapisać wynik z powrotem lub co gorsza, rozpropagować go po większej ilości kluczy. Celem ogólnym jest zmniejszenie ilości interakcji po sieci przez przeniesienie części logiki właśnie do Redisa.
Czekaj, stop!
Podoba Ci się to co tworzę? Jeśli tak to zapraszam Cię do zapisania się na newsletter:Jeśli to Cię interesuje to zapraszam również na swoje social media.
Jak i do ewentualnego postawienia mi kawy :)
Żeby można było tego dokonać, trzeba umieć wykonywać komendy Redisowe ze skryptów Lua. I znów, najłatwiej będzie na przykładzie:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | from redis import Redis redis_connection = Redis(decode_responses=True, db=0) redis_connection.set("key1", 10) script = """ local arg1 = redis.call('get','key1') redis.call('set', 'key2', arg1 + KEYS[1]) return nil """ print(redis_connection.eval(script, 1, 5)) # None, ze względu na "return nil" print(redis_connection.get("key2")) # 15, bo 10 + 5 = 15 ;) |
Powyższy kod kazał Redisowi wykonać skrypt w którym odczytuję wartość z pod klucza „key1”, dodaję do niej wartość przekazaną jako argument i zapisuję nową wartość w kluczu o nazwie „key2”. Poza „redis.call” istnieje jeszcze „redis.pcall”, pozwalający na inne podejście do ewentualnej obsługi błędów, po więcej szczegółów odsyłam jednak do dokumentacji.
Bardziej realny przykład
Przeanalizuj taki kod:
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 | from redis import Redis redis_connection = Redis(decode_responses=True, db=0) permission = 'ADD_BOOKING' redis_connection.sadd("users_group:2", *list(range(0, 50))) redis_connection.sadd('permissions', permission) # dotąd jest przygotowanie danych add_permission_script = """ local is_valid_permission = redis.call('sismember', 'permissions', KEYS[2]) if is_valid_permission == 1 then local users = redis.call('smembers','users_group:'..KEYS[1]) for _, user in ipairs(users) do redis.call('sadd', 'user_permissions:'..user, KEYS[2]) end return true else return false end """ print(redis_connection.eval(add_permission_script, 2, 2, permission)) |
Ten kod dodaje konkretne uprawnienie wszystkim użytkownikom którzy są w zadanej grupie. Początek tego snippetu to przygotowanie danych, określamy nazwę uprawnienia, wypełniamy grupę użytkownikami (ich ID oczywiście) i dodajemy uprawnienie do listy przechowującej wszystkie możliwe uprawnienia. Jako argumenty do skryptu Lua przekazujemy ID grupy i uprawnienie które chcemy rozpropagować. Sam skrypt pierw sprawdza czy uprawnienie jest poprawne (znajduje się w zbiorze poprawnych) i jeśli tak to dla każdego użytkownika który należy do przekazanej grupy, dodaje uprawnienie. Ewentualna duplikację uprawnień załatwiamy odpowiednim typem danych, klucz „user_permissions:ID” jest SETtem, więc nie pozwala na duplikacje.
Podobną logikę można zrealizować w Pythonie oczywiście, ale zwiększa to ilość danych które są przesyłane przez sieć, bo rozbiło by nam to tę sytuację na minimum 3 odpytania Redisa. I to zakładając, że ktoś ostatni wpis rzetelnie przeczytał. A i skrypty są w pełni atomowe!
Ale czekaj.. Przecież ten skrypt jest długi. A pewnie szybko i dłuższe byłyby konieczne. To co z tym oszczędzaniem na przesyłaniu danych?
Cache skryptów
Przesyłanie za każdym razem ciała skryptu nie dawałoby oszczędności na ilości danych przesyłanych przez sieć. Redis pozwala na zapisanie go u siebie i posługiwanie się skrótem SHA w celu jego uruchomienia. Skrypt zapisujemy komendą SCRIPT LOAD gdzie jako argument podajemy skrypt a zwraca on nam skrót, którego możemy użyć w celu uruchomienia naszego kawałka kodu LUA:
1 2 | sha = redis_connection.script_load(add_permission_script) print(redis_connection.evalsha(sha, 2, 2, permission)) |
Zamieniając EVAL na EVALSHA.
Podsumowując
Przeszliśmy sobie przez jedną z ciekawszych opcji jakie oferuje Redis. Skrypty to potężne narzędzie, które pozwala na grupowanie kodu i oszczędność w kontekście ilości danych przesyłanych przez sieć.
Mateusz Mazurek