«Хранимые процедуры» в 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

Как «правильно»:

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

Пожалуйста, проголосуйте за тему, которую стоит рассмотреть в следующей статье:

Никто ещё не голосовал. Воздержавшихся нет.

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

ссылка на оригинал статьи http://habrahabr.ru/post/270251/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *