NoSQL*, Lua*

Разработка → «Хранимые процедуры» в Redis

alexeyknyshev 8 ноября 2015 в 16:25 16,1k
image

Многие знают про возможность хранить процедуры в sql базах данных, про это написано немало пухлых руководств и статей. Однако мало кто знает, что схожие возможности имеются и в Redis, начиная с версии 2.6.0. Но так как Redis не является реляционной БД, то и принципы описания хранимых процедур достаточно сильно отличаются. Хранимые процедуры в Redis — практически полноценные Lua скрипты (на момент написания статьи в качестве интерпретатора используется Lua 5.1).

Дальнейшее повествование предполагает базовое знакомство с API Redis, а также, что процесс redis-server запущен на localhost:6379. Если вы новичок в Redis, то вам стоит перед прочтением следующего материала ознакомиться с краткой информацией о том, что такое Redis. А также пройти, хотя бы частично данное интерактивное руководство.

Hello world!


Используя redis-cli вернём из базы строку «Hello world!»:
redis-cli EVAL 'return "Hello world!"' 0

Результат:
"Hello world!"

Давайте разберёмся, что только что произошло:
  1. Вызов встроенной в Redis команды EVAL с двумя аргументами. Первый
    return "Hello world!"
    — тело функции Lua.
    0
    — количество ключей Redis, которое будет передано в качестве параметров нашей функции. Пока мы не передаём ключи redis в качестве параметров, т.е. указываем 0.
  2. Интерпретация текста программы на сервере и возврат Lua-string значения
  3. Преобразование Lua-string в redis bulk reply
  4. Получение результата в redis-cli
  5. redis-cli выводит bulk reply на stdout


Хранимые процедуры в Redis это обычные функции Lua, а следовательно и принцип получения и возврата аргументов аналогичен.
Замечание: Lua поддерживает mul-return (возврат более чем одного результата из функции). Но чтобы возвратить несколько значений из redis, нужно использовать multi bulk reply, а из Lua в него отображаются таблицы, пример ниже не будет работать так, как вы возможно ожидаете:
redis-cli EVAL 'return "Hello world!", "test"' 0

"Hello world!"

Результат усекается до одного возвращаемого значения (первого).

Hello %username%!


Двигаемся дальше. Так как функции без аргументов особого интереса не представляют, добавим обработку аргументов в нашу функцию.
Согласно документации функция, выполняемая через EVAL, может принимать произвольное количество аргументов через Lua таблицы KEYS и ARGV. Воспользуемся этим, чтобы поприветствовать %username%, если строка, содержащая его имя, передана в качестве аргумента, а иначе поприветствуем Habr.

Вызываем без аргументов, массив-таблица ARGV в Lua пустая, т.е и ARGV[1] вернёт nil
redis-cli EVAL 'return "Hello " .. (ARGV[1] or "Habr") .. "!"' 0

Результат:
"Hello Habr!"

А теперь в качестве параметра передадим строку «Иннокентий»:
redis-cli EVAL 'return "Hello " .. (ARGV[1] or "Habr") .. "!"' 0 'Иннокентий'

Результат:
"Hello \xd0\x98\xd0\xbd\xd0\xbd\xd0\xbe\xd0\xba\xd0\xb5\xd0\xbd\xd1\x82\xd0\xb8\xd0\xb9!"

Замечание: Redis хранит строки в utf8 и для того, чтобы избежать каких-либо проблем на стороне клиента в redis-cli символы, не входящие в ascii, выводятся в виде escape последовательностей. Чтобы увидеть читаемую строку в bash можно сделать так:
echo -e $(redis-cli EVAL 'return "Hello " .. ARGV[1] .. "!"' 0 'Иннокентий')


Доступ к API Redis из скриптов


В каждый Lua скрипт интерпретатор загружает эти библиотеки:
string, math, table, debug, cjson, cmsgpack

Первые 4 — стандартные для Lua. 2 последние — для работы с json и msgpack соответственно.

Для того чтобы взаимодействовать с данными в нашем хранилище в Lua экспортирован модуль 'redis'. Воспользовавшись функцией call в данном модуле, мы можем выполнять команды в формате, соответствующем командам из redis-cli.

Рассмотрим использование redis.call на примере скрипта, который проверяет, существует ли пользователь в нашей базе, а если существует, то проверяет соответствие пары логин — пароль.

Создадим в нашей базе тестовый набор данных, содержащий пары логин — пароль.
redis-cli HMSET 'users' 'ivan' '12345' 'maria' 'qwerty' 'oleg' '1970-01-01'

OK


Убедимся, что всё действительно ОК:
redis-cli HGETALL 'users'

1) "ivan"
2) "12345"
3) "maria"
4) "qwerty"
5) "oleg"
6) "1970-01-01"


На вход скрипту будем подавать один аргумент, json строку в формате:
{
"login":"userlogin",
"password":"userpassword"
}


Скрипт, должен возвращать 1, если пользователь существует и пароль в json совпал с паролем в базе, иначе 0. Если входной формат ошибочен, например не был передан аргумент скрипту (ARGV[1] == nil) или в json отсутствует одно из требуемых полей, возвратим читаемую строку, содержащую информацию об ошибке.

Для разбора и упаковки json redis экспортирует в Lua модуль cjson. В нашем скрипте мы воспользуемся функцией decode из данного модуля. В качестве параметра функция принимает Lua-string, в которой содержится json, а возвращаемым значением является Lua-таблица, строковыми ключами которой являются json-поля.

Создадим файл login.lua со следующим содержимым.
Код скрипта login.lua
local jsonPayload = ARGV[1]

if not jsonPayload then
    return 'No such json data'
end

local user = cjson.decode(jsonPayload)

if not user.login then
    return 'User login is not set'
end

if not user.password then
    return 'User password is not set'
end

-- вызов redis API из Lua аналогичен стандартному API redis.
local expectedPassword = redis.call('HGET', 'users', user.login)
if not expectedPassword then
    return 0
end

if expectedPassword ~= user.password then
    return 0
end

return 1



Примеры использования:
  1. Пароли совпадают
    redis-cli EVAL "$(cat login.lua)" 0 '{"login":"maria","password":"qwerty"}'
    

    (integer) 1
    

  2. Пароли не совпадают
    redis-cli EVAL "$(cat login.lua)" 0 '{"login":"maria","password":"12345"}'
    

    (integer) 0
    

  3. В json отсутствует поле с паролем
    redis-cli EVAL "$(cat login.lua)" 0 '{"login":"maria","pwd":"12345"}'
    

    "User password is not set"
    

  4. Не передан аргумент, содержащий json
    redis-cli EVAL "$(cat login.lua)" 0
    

    "No such json data"
    



Замечание: Всё ключи в Redis, а также работа с ними через SET и GET, имеют строковое представление. В Redis нет типа integer, и float тоже нет. Важно это понимать. В следующем примере мы возвращаем значение ключа test как строку:
redis-cli SET test 5

OK

Узнаем тип хранимого значения:
redis-cli TYPE test

string

Вернём, но уже через скрипт:
redis-cli EVAL "return redis.call('GET', 'test')" 0

"5"


При этом нам никто не запрещает вернуть integer (в качестве integer bulk reply):
redis-cli EVAL "return tonumber(redis.call('GET', 'test'))" 0

(integer) 5


Будьте осторожны с передачей Lua-number в качестве параметра функции redis.call:
redis-cli EVAL "return redis.call('SET', 'test', 5.6)" 0

OK

Значение усекается до меньшего целого
redis-cli EVAL "return tonumber(redis.call('GET', 'test'))" 0

(integer) 5

Но что же там действительно внутри:
redis-cli GET test

"5.5999999999999996"

Как «правильно»:
redis-cli EVAL "return redis.call('SET', 'test', tostring(5.6))" 0

OK

redis-cli GET test

"5.6"


По всей видимости преобразование Lua-number идёт не в интерпретаторе Lua, а в нативной части Redis, написанной на Си.

На сегодня всё.

Смотрите также


redis.io/commands/eval
www.redisgreen.net/blog/intro-to-lua-for-redis-programmers
redislabs.com/blog/5-methods-for-tracing-and-debugging-redis-lua-scripts
Проголосовать:
+16
Сохранить: