«Хранимые процедуры» в Redis

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

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

© Habrahabr.ru