«Хранимые процедуры» в Redis
Многие знают про возможность хранить процедуры в 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!"
Давайте разберёмся, что только что произошло:
- Вызов встроенной в Redis команды EVAL с двумя аргументами. Первый
return "Hello world!"
0
- Интерпретация текста программы на сервере и возврат Lua-string значения
- Преобразование Lua-string в redis bulk reply
- Получение результата в redis-cli
- 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 со следующим содержимым.
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
Примеры использования:
- Пароли совпадают
redis-cli EVAL "$(cat login.lua)" 0 '{"login":"maria","password":"qwerty"}'
(integer) 1
- Пароли не совпадают
redis-cli EVAL "$(cat login.lua)" 0 '{"login":"maria","password":"12345"}'
(integer) 0
- В json отсутствует поле с паролем
redis-cli EVAL "$(cat login.lua)" 0 '{"login":"maria","pwd":"12345"}'
"User password is not set"
- Не передан аргумент, содержащий 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
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.