Гайд по использованию Lua-скриптов в Redis

Что такое язык Lua?

Язык программирования Lua появился в 1993 году. По своей структуре он является компактным языком программирования, который можно встраивать практически в любое приложение — от World of Warcraft до веб-сервера Nginx. Ну и конечно же, в Redis, о котором далее и пойдёт речь.

Благодаря Lua в Redis возможно встраивать собственные скриптовые расширения для базы данных. Вызов скриптов выглядит следующим образом:

> EVAL 'local val="Hello Compose" return val' 0
"Hello Compose"

Далее сам Lua-скрипт:

local val="Hello Compose"
return val

Что более важно, вы можете запускать скрипты в качестве «умных транзакций». Это позволяет обрабатывать ошибки в рантайме, не останавливая приложение. Безусловно, то, насколько «умными» будут эти транзакции, зависит целиком и полностью от вас.

Ключи и аргументы

Для обращения к скрипту неплохо бы было передать ему ключи и аргументы. Например, как в нижеприведённом коде:

> EVAL 'local val="Hello Compose" return val' 0

В конце EVAL мы видим ноль — это количество ключей, переданных скрипту. Но если вместо ноля написать 2 foo bar fizz buzzthen, первые два элемента foo и bar будут переданы в качестве ключей, а fizz и buzz — в качестве аргументов.

Ключи доступны Lua-скрипту в таблице KEYS. Таблица в Lua — это ассоциативный массив, который также используется в качестве массива из одного элемента. Если помимо ключей используются аргументы, они будут доступны в таблице ARGV, например:

return ARGV[1]..' '..KEYS[1]

В Lua .. используется в качестве оператора объединения, так что здесь в качестве возвращаемого значения мы получаем аргумент, привязанный к пробелу, и название ключа, которое было передано в качестве аргумента:

> EVAL "return ARGV[1]..' '..KEYS[1]" 1 name:first "Hello"
"Hello name:first"

Никакой магии в KEYS, это просто строка, так что нам по-прежнему нужно получить их значение.

Что касается вызова Redis из Lua, можно использовать функцию redis.call(). Например:

> EVAL 'return ARGV[1].." "..redis.call("get",KEYS[1])' 1 name:first "Hello"
"Hello Brian"

Прим. автора Если в процессе объединения вы получаете ошибку «attempt to concatenate a boolean value» (попытка объединения значений булевого типа), скорее всего name:first не было присвоено какое-либо значение.

Таким образом, скрипт взял параметр, нашёл значение ключа и вернул в качестве вывода созданную строку.

Однако помимо EVAL есть и другой способ передать скрипт на сервер. Например, используя аргументы команды redis-cli. Попробуем написать чуть более сложный скрипт:

lua
local name=redis.call("get", KEYS[1])
local greet=ARGV[1]
local result=greet.." "..name
return result

Сохраним его как longhello.lua и выполним в командной строке:

$ redis-cli -h aws-us-east-1-portal.15.dblayer.com -p 11260 -a secret  --eval longhello.lua name:first , Hello
"Hello Brian"

Итак, мы запускаем redis-cli, добавляем параметры -h, -p и -a для связи с базой данных. Затем идёт --eval, позволяющий указать имя файла, в виде которого скрипт отправится на сервер. Также нашему скрипту понадобятся ключи и аргументы. В данном случае ключами являются указанные до первой запятой параметры команды.

Сложные скрипты

Следующий случай наглядно демонстрирует, как Lua решает одну небольшую проблему. Допустим, разные пользователи или разработчики увеличивают счётчики в большой структуре данных. Например, region:one увеличивает count:emea, count:usa, count:atlantic, в то время как region:two затрагивает лишь count:usa. Эти счётчики могут быть добавлены позже, но вдруг вам важно убедиться, что добавятся они все разом? Самое время вспомнить про «умные транзакции».

Добавим все наши регионы в список:

> rpush region:one count:emea count:usa count:atlantic
(integer) 3
> rpush region:two "count:usa"
(integer) 1

Создадим локальную переменную:

local count=0

Начнём с переменной-счётчика — он будет считать все операции по увеличению наших округов.

local broadcast=redis.call("lrange", KEYS[1], 0,-1)

Теперь запросим у Redis все значения списка, относящиеся к первому ключу.

for _,key in ipairs(broadcast) do

Здесь мы запустили цикл, в котором функция ipairs() просмотрит каждую задействованную Lua-таблицу и передаст из неё ключ.

redis.call("INCR",key)
count=count+1
end
return count

С каждым вызовом мы увеличиваем указанный ключ, а заодно и наш счётчик.

После окончания цикла мы получаем конечное число итераций. Сохраним итоговый скрипт и запустим его на сервере:

$ redis-cli -h aws-us-east-1-portal.15.dblayer.com -p 11260 -a secret --eval broadcast.lua region:one
(integer) 3

В таблице будут содержаться следующие значения:

> mget count:usa count:atlantic count:emea

1) "1"

2) "1"

3) "1"

Значения для region.two:

$ redis-cli -h aws-us-east-1-portal.15.dblayer.com -p 11260 -a secret --eval broadcast.lua region:two
(integer) 1
> mget count:usa count:atlantic count:emea
1) "2"
2) "1"
3) "1"

Но что, если бы во время выполнения скрипта произошла ошибка? Он просто продолжил бы выполняться, игнорируя её. Для вывода деталей ошибки следует использовать redis.pcall().

Кэширование скриптов

Для того чтобы не загружать скрипт каждый раз перед выполнением, можно использовать команду SCRIPT LOAD для загрузки скрипта в кэш. Рассмотрим пример использования из командной строки:

$ redis-cli -h aws-us-east-1-portal.15.dblayer.com -p 11260 -a secret SCRIPT LOAD "$(cat broadcast.lua)"
"84ffc8b6e4b45af697cfc5cd83894417b7946cc1"

Здесь $(cat broadcast.lua) превращает наш скрипт в аргумент. А шестнадцатеричное число ниже — SHA1-подпись нашего скрипта. Её можно использовать для его последующего вызова командой EVALSHA:

> EVALSHA 84ffc8b6e4b45af697cfc5cd83894417b7946cc1 1 region:one
(integer) 3

Также есть команды для проверки наличия скрипта на сервере и его удаления — SCRIPT EXISTS и SCRIPT FLUSH соответственно.

Не всё так просто

После довольно небольшого промежутка времени (по умолчанию — 5 секунд) Lua-скрипты начнут выдавать ошибки в ответ на запросы — в таком случае возможно только «убить» скрипт командой KILL SCRIPT либо выключить сервер командой SHUTDOWN NOSAVE. С другой стороны, 5 секунд — очень щедрое ограничение, ведь ваши скрипты должны выполняться буквально в течение миллисекунд. И на это есть очень веская причина: во время выполнения ваших скриптов все остальные процессы приостанавливаются.

Заключение

Итак, в этой статье мы рассмотрели примеры написания простых и сложных Lua-скриптов, их вызов из Redis, а также запись и хранение на сервере.

Перевод статьи «A Speed Guide To Redis Lua Scripting»

Подобрали два теста для вас:
— А здесь можно применить блокчейн?
Серверы для котиков: выберите лучшее решение для проекта и проверьте себя.