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
Пожалуйста, проголосуйте за тему, которую стоит рассмотреть в следующей статье:
68%
(48)
Подробнее рассказать про работу с Lua API. Работа со multi-bulk reply
32%
(23)
Рассмотреть использование redis в связке с Go, используя radix.v2

Проголосовал 71 человек. Воздержался 21 человек.

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

Комментарии (44)


  1. datacompboy
    08.11.2015 17:14

    Таки они хронятся в redis, или только исполняются?


    1. alexeyknyshev
      08.11.2015 17:20
      +2

      команда EVAL именно интерпретирует строку, т.е. фактически делает то же самое, что и eval во многих скриптовых языках. Однако вы можете сохранять скрипты, и это предполагается делать, если вы используете их в продакшене. Для этого вы должны вызвать SCRIPT LOAD «текст скрипта», он вернёт вам SHA1 строку-хеш, по этому хешу вы потом можете вызвать скрипт, используя EVALSHA %SCRIPT_SHA%, также, как делаете это с EVAL.


      1. datacompboy
        08.11.2015 17:32

        То есть «заменить» скрипт нельзя?


        1. alexeyknyshev
          08.11.2015 17:43
          +1

          Заменить с тем же SHA нельзя, однако можно сбросить скриптовый кеш командой SCRIPT FLUSH, т.е. удалить все скрипты и загрузить заново. Скриптовый кеш живёт до тех пор, пока redis-server не будет перезапущен. Однако это не является большой проблемой, если вы конечно не загружаете скрипты в огромном количестве.


          1. datacompboy
            08.11.2015 17:57
            +1

            Ну то есть сохранить интерфейс server-side нельзя, это только экономия на размерах вызовов.


            1. bak
              08.11.2015 18:22
              +2

              Мало того — в случае рестарта сервака их нужно загружать повторно. Напоролся на проде на это :)


      1. datacompboy
        08.11.2015 17:33
        +1

        p.s.: вот если бы EVALSHA работало без SCRIPT LOAD!!!


        1. alexeyknyshev
          08.11.2015 17:44
          +2

          Не понимаю, как это возможно. Ведь сначала нужно загрузить скрипт, а уже потом исполнять.


          1. datacompboy
            08.11.2015 17:57
            -1

            Ну как, SHA1 обращаем и выполняем :)


        1. AxVPast
          08.11.2015 21:56

          Проблема вобщем-то очень просто решается на стороне драйвера редиса.
          Стандартный NodeJS драйвер сначала, по принятому тексту скрипта, считает sha. Пытается сделать evalsha. Если не получается — делает eval.
          Следующий вызов этого-же текста программы приводит к тому, что исполнится именно evalsha.

          Решение реально работающее с любыми скриптами, без потенциальных утечек памяти, но не оптимальное.
          Удобно сделать кеширующую прослойку — сделать фабрику, которая берет на вход текст скрипта, по нему считае sha и запоминает, а на вход выдет функцию, в которой можно менять только параметры (а не тело скрипта). В результате если скрипты большие — многократного пересчета sha по тексту скрипта не просиходит.


  1. AlexeiZhuravlev
    08.11.2015 21:28
    +2

    Пару месяцев назад пришлось воспользоваться скриптами, чтобы быстро почистить базу от неактуальных данных.

    Так вот в lua-скриптах для Redis есть одна недокументированная особенность: там разделено получение данных и их удаление. То есть нельзя в одном скрипте получить список ключей и тут же вызвать их удаление. Redis — это просто не дает сделать. Пришлось писать два скрипта — один формирует список ключей и потом с помощью xargs передаем его второму скрипту для удаления.

    В документации указана основная причина использовать скрипты — снижение накладных расходов на пересылку данных и как следствие скорость работы по сравнению с программами обращающимися к редис на прямую.

    Для сравнения мной была написанная программа на Go для поиска ключей и удаления. И она выполнялась быстрее lua-скриптов.

    Так что мой опыт показывает, что lua-скрипты в Redis есть больше для галочки. Они не оптимизированы для быстрой работы.


    1. AxVPast
      08.11.2015 21:41
      +3

      Вы что-то не так делали (или у Вас какая-то очень древняя вресия Редиса). В реальности lua скрипт это такой-же клиент к редису как и программа на Go (для ввода вывода в редис используется redis протокол и входной/выходной поток = TCP/IP поток). Единсвтенное отличие — lua получает эксклюзивное владение памятью редиса на момент исполнения. Если Вы прочитали все ключи на удаление в 1 обращение (и не вылетели по памяти) то все вообще должно было пройти на отлично. Если Вы таки использовали scan — почитайте как работают там курсоры — занимательное чтиво — и то «странное» число которое выдает scan — и есть реальный курсор свернутый в число. Кстати, при работе с курсором они позволяют и удалять данные, единственное можно получить 1 значение более чем 1 раз (особенность реализации курсоров).


      1. AlexeiZhuravlev
        08.11.2015 22:40
        +1

        Я именно что использовал SCAN чтобы найти какие ключи удалить. И после получения списка ключей не мог вызвать HDEL для удаления в этом же скрипте.


        1. AxVPast
          09.11.2015 03:34
          +1

          Скорее всего Вы работали со слейвом. Такое часто делается когда тяжеловесный скан запускается на слейве (который read-only), а потом реальные изменения выполняются на мастере (который вообще на другом сервере находится).
          scan — не магический вызов, он возвращает курсор, который есть некое магическое число и редис ни коим образом не запоминает, что Вы его вообще в принципе вызывали. А если Вы вычитали все данные — то вообще нет никаких причин Вам запрещать что-то удалять. Проведите простой эксперимент — в скрипте создайте ключ, его найдите через скан и удалите. И если не работает — то на которой версии редиса?


          1. AlexeiZhuravlev
            09.11.2015 10:50
            +1

            Нет. У меня была только одна база без слейвов. В ней было 700 млн ключей(примерно 60 Гб), удалил я в итоге 550-570 млн ключей. Версия редиса 2.6.16.


            1. AxVPast
              09.11.2015 22:27
              +1

              Я был не прав. Просто обычно приходилось работать с сортированными коллекциями и читать/удалять в упоряоченном порядке.
              Это и правда не работает:

              local function scanAndDeleteKeys()
              
                  local scanRes = redis.call('scan', 0, 'MATCH', 'rootKey:*')
              
                  redis.call('hmset', 'rootKey:999', 'a', 'a999', 'b', 999 );
              
                  return 0
              
              end
              

              И еще не работает тут:

              {«spop»,spopCommand,-2,«wRsF»,0,NULL,1,1,1,0,0},

              {«srandmember»,srandmemberCommand,-2,«rR»,0,NULL,1,1,1,0,0},

              {«sscan»,sscanCommand,-3,«rR»,0,NULL,1,1,1,0,0},

              {«zscan»,zscanCommand,-3,«rR»,0,NULL,1,1,1,0,0},

              {«hscan»,hscanCommand,-3,«rR»,0,NULL,1,1,1,0,0},

              {«randomkey»,randomkeyCommand,1,«rR»,0,NULL,0,0,0,0,0},

              {«scan»,scanCommand,-2,«rR»,0,NULL,0,0,0,0,0},

              {«lastsave»,lastsaveCommand,1,«rRF»,0,NULL,0,0,0,0,0},

              {«pubsub»,pubsubCommand,-2,«pltrR»,0,NULL,0,0,0,0,0},


              1. AlexeiZhuravlev
                10.11.2015 11:38

                Вы целое мини-исследование провели :-)


                1. AxVPast
                  10.11.2015 18:10

                  Кстати тут видно очевидный баг — нет вызова info, а он на разных серверах вернет разные значения.


  1. AxVPast
    08.11.2015 21:32
    +2

    Автор неглубоко копал. В некоторых местах поправлю.

    В редисе все-таки есть тип integer. И для тех, кто пишет хранимые процедуры это важно знать.
    Например, мы создаем ключ 'set ourIntKey 1'. Дальше можно сделать 'incrby ourIntKey 1'. Пока все вроде бы хорошо, но мы можем попробовать это сделать из скрипта на lua:
    redis.call('incrby', 'ourIntKey', 1);
    теперь усложним и попробуем сделать так redis.call('incrby' 'orIntKey', 0.01) — программа слетела? Сюрприз в lua любые числа — double.
    В последней версии Redis есть тип данных float и его тоже можно добавлять и вычитать.

    Вторая часть которую следовало написать — все что Вы отправляете в ердис как eval — по нему всегда считается sha1 и используется для кеширования скомпилированной хранимой процедуры. В реальности в Редисе есть еще один корень ключей: которые не доступны «снаружи» и где хранятся скомпилированны скрипты и их тексты (нужны для репликации). Таким образом можно себе организовать утечку памяти делая eavl для постоянно мутирующего тела скрипта (например, всегде клеить его перед вызовом и включить внутрь параметры). Оно врое логично сделать так:

    eval 'local a = ' + a '\nlocal b = ' + b + '\nreturn a+b'

    Только вот не стоит так делать.

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

    local function aPlusBFactory()
    return function (a, b)
    return math.floor(a + b)
    end
    end


    1. alexeyknyshev
      08.11.2015 22:22

      Что касается утечки, то каюсь, не подумал об этом написать, т.к. думал, что это очевидно, ведь это применимо и ко многим sql бд. В частности, если конструировать запросы не через prepare + bind, а в лоб формируя строку запроса, то можно запросто израсходовать лимит дескрипторов в том же oracle, а в дополнение и получить дыру в безопасности, в лице возможности формирования входных данных для sql-инъекции.


    1. alexeyknyshev
      08.11.2015 22:23

      Про тип я говорил, как про внутреннее представление данных. То что у нас есть возможность работать со строками как с integer я не отрицаю.


      1. AxVPast
        09.11.2015 03:36

        Есть еще один секрет — многие строки (до 22 букв) в редисе это 64битный Integer. Вобщем в Редис скорее наоборот — больше чисел чем текста.


  1. AlexeiZhuravlev
    08.11.2015 21:33

    P.S. дополню свой комментарий, так как сразу не заметил голосование после статьи. Для программы на Go я тоже использовал radix.v2.

    Но где-то недавно была информация что библиотека Redigo быстрее чем radix.v2. Так что может быть можно написать еще более быстрый скрипт для чистки базы, чем получилось у меня.


    1. sergeylanz
      08.11.2015 22:11

      Я использую а продакшине Redigo. Доволен очень. Удобный api даже смог красиво прикрутить newrelic на запросы в редис. Одно плохо есть проблема писать struct которые не только string а потом их читать надо их переводит в json а потом обратно. Немножко накдладно было. заменил json на messagepack с статической серилизацией получилось очень быстрая вещь


      1. AlexeiZhuravlev
        08.11.2015 22:45

        Я не в рамках этой программы сначала использовал для подключения к редис gopkg.in/redis.v3, но этот оказалось полное извращение. (Отдельная функция для каждой команды редис. Причем каждая возвращает свой набор параметров.) Уже потом увидел более-менее сносный radix.v2 По поводу Redigo — его еще не смотрел, только читал, что он быстрее.


        1. sergeylanz
          08.11.2015 22:48

          redigo все проще

          data, err := c.Do(command, args...)
          


    1. alexeyknyshev
      08.11.2015 22:16

      Насколько мне известно, у скриптов в redis одна основная задача, сократить все обращения к базе до одного round-trip при этом обеспечить атомарность. Действительно странно, что у вас получалось достичь большей скорости на go. Ваша программа на go последовательно вызывала комманды или же вы делали через транзакции multi


      1. AlexeiZhuravlev
        08.11.2015 22:34
        +1

        Использовал pipelining


  1. misterion
    08.11.2015 22:11
    +2

    Важно ещё помнить про то, что в Lua у вас есть доступ не ко всем функциям редиса. К примеру нельзя использовать SRANDMEMBER, что фактически не позволит вам писать процедуры с логикой «Вернуть N случайных значений из SET» или семейство SCAN функций. Т.е. скажем сделать какой-то аналог «воркера», который выполнит операцию над семейством ключей или полей в LUA не выйдет. Помните, что у вас не будет человеческой отладки, если вы решили писать что-то действительно сложное, что до сих пор нет нормальных библиотек для тестирования LUA процедур. Насколько я знаю ни на одном языке (хорошая идея для проекта на гитхаб). Это совсем не обычная ситуацию, но помните про ограничение с LUAI_MAXCSTACK. Помните, что любые битовые операции в LUA удивят вас тем, что при поддержки 64 битных целых чисел все битовые операции ограничены 32 битными со знаком. Помните, что фактически у вас нет скриптов, если вы используете кластер, т.к. нет авто переадресации запросов на соседние ноды. LUA в редисе прекрасно позволяет решать огромный спектр вопросов, но это совсем не панацея.


    1. AxVPast
      09.11.2015 03:40

      Это логично, что не работает генератор случайных чисел и все функции получения текущего времени (хотя это ограничение можно обойти, но это извращенный способ самоубийства). Иначе не будет работать репликация данных на redis — slave и не будеть возможности поднимать редис из AOF (если совсем все плохо получилось).


      1. misterion
        09.11.2015 10:14

        Да, вы правы. С какой логикой это сделал Сальваторе понятно и я считаю, что это скорее ограничение корявой реализации репликации/интеграции LUA для части команд. К примеру встроенный генератор случайных числе из самой LUA вам доступен. И часть танцев с бубном вокруг SRANDMEMBER так и работает — получите весь список, кидайте кубик в LUA и формируйте таблицу выдачи. В redis много такого — в конкретной фиче всё супер, но чего-то, самую малость, не хватает.


        1. AxVPast
          09.11.2015 10:21

          Это не решение. Проблема в том, что скрипт может менять миллионы ключей и обращаться к генераторам миллионы раз. Когда происходит репликация на слейв — то реплицируются не изменения, которые делает скрипт, а вызов самого скрипта (то есть на слейве происходит повторное выполнение скрипта). То-же саме происходит и при поднятии базы из AOF. Другими словами — решение отличное для решаемой проблемы. Да с некоторыми (весьма логичными) ограничениями.


          1. misterion
            09.11.2015 19:31

            Я понимаю как работает репликация, ровно как и ограничения с этим связанные. Попробуйте посмотреть на это с такой стороны. Сразу после релиза 2.6 (в котором добавили LUA) сразу же подняли вопрос и про RANDOMKEY и про SRANDMEMBER. Это весьма логично, т.к. сама структура SET подразумевает, что вам для широкого спектра бизнес задач нужно получать из неё N случайных элементов (не даром в 2.6 в эту функцию добавили второй аргумент — количество случайных элементов). Когда впервые поднимался этот вопрос Сальваторе предлагали альтернативу с тем, чтобы переписать репликацию для SET с тем, чтобы свести внутреннюю структуру SET на слейве и мастере — тогда возможно было бы обращаться к элементам сета по индексам. Он ответил в духе — «Это не возможно». К чему я так цепляюсь к этому случае — в редис почти всё так. Смотрите, я отвечаю (и читаю) вопросы по редису на stackoverflow. Аудитория продукта (а редис продукт, который Сальваторе продаёт) часто задаёт вопросы, ответ на который — используйте уникальный список с произвольным доступом. Т.е. или в LIST возможность взять индекс по значению (LRANK request) или в SET получить аналог LRANGE (простите на память не помню как назывался тикет на гитхабе). И тогда можно на LUA можно эффективно решать этот класс задач. И на оба вопроса ответ — «Напишите как нить сами на LUA». Это, имхо, вопрос отношения показывающий отношения основного мейнтейнера в своему продукту. К слову, в том же Tarantool Костя Осипов с командой идёт от решения конкретных проблем и кейсов своих пользователей, вместо отсылок в случае редиса. Как и в 2.8 (поправьте если путаю) вхерачили Global variables protection в LUA — на все вопросы оставьте хотя бы в конфиге шанс выключить это ответ «Не, а то вдруг что». Только не дали в замен никаких вариантов шарить между скриптами вспомогательные функции (а их, к слову, превиликое множество получается при написании чего-то длиннее пары строчек). Простите, накипело.


            1. AxVPast
              09.11.2015 22:43

              Не, а то вдруг что:
              tonumber = function(x)
              — И привет Вашему ёжику в соседнем скрипте :)
              end

              Логично, что запретили глобальные переменные, так как становится вообще не понятно, что реплицировать. У них и так была проблема с рекликацией скрипта который иногда что-то меняет. То есть первые пару выполнений ничего не делает, а потом начинает менять состояние редиса. В результате слейву забывали рассказать, что такой скрипт существует и слейв реально начинал слетать.

              А по поводу переиспользования функий — можно сделать очень просто:
              Делаем каталог, в него складываем все lua скрипты. Делаем загрузчик каталога, который сохраняет все скрипты в строковые константы. Потом просто перед вызовом «клеим», что хотели и делаем «return targetFunction(KEYS)» в конце и пропускаем через фабрику, которая считает sha. Получаем отличный способ шарить функции + пре-подсчитанный композит который почти сразу позволяет делать evalsha.

              Так-же можно построить и тестирование — отдельно загружать функции (просто склейкой строк) и отдельно тестировать каждую функцию.
              Выглядит где-то так:
              var targetFunctionScript = scripts.comonFunctions + scripts.additionalFunctins + scripts.superFunctions + 'return targetFunction(KEYS[1], KEYS[2])';
              var targrtFunctionCall = evalFactory( someRedis, targetFunctinsScript, 2 );

              Дальше просто вызываем:
              return targetFunctionCall( 'a', 'b'). then( function (value)){…


              1. misterion
                09.11.2015 22:59

                Поймите, я не хочу сказать, что решения нет. Я хочу сказать, что в соседнем огороде (в том же тарантуле) люди обошлись без костылей. И в редисе могли бы. Проблемы с репликой и LUA были ещё на этапе alpha preview, то, что будут ограничения было понятно ещё до выхода фичи в их стабильную ветку. Я настаиваю на том, что разработчиками стоило бы поправить проблемы в проектировании репликации вместо цикла решений об ограничении функциональности скриптовой части. В любом случае мы с вами про одно и тоже, просто немного с разных сторон.


                1. AxVPast
                  09.11.2015 23:48

                  Судя по документации на Тарантул и Редис — у них разное происхождение. Редис вырос из обычной либы, которая более менее эффективно реализовала структуру hashset и предоставила, используя примитивный протокол (в 4 команды), доступ к этому hashset из других процессов. Redis это намного лучше чем просто использование между процессами шареной памяти. Потом в процессе роста к этой простой системе докрутили репликацию и «какой-то там скриптовый язык» чтобы решить проблему его не транзакционности. Причем прикрутили относительно «красиво» — через тот-же redis протокол да еще и «положили» функции в этот-же hashset. При прикурчивании «отгребли» кучу проблем с тем, что якобы «герметичная» среда исполнения не совсем герметична и не совсем подходит под изначальную концепцию протого хранилища типа key-value в плане AOF и репликации.

                  Тарантул наоборот декларируется практически как application server для lua, причем создавался таким с самого начала.

                  Не удивительно, что Редис с Lua работает несколько странно :).


                  1. rtsisyk
                    10.11.2015 09:30

                    Тарантул вырос как база данных (с транзакциями, репликацией и т.п.), это потом оказалось, что пользователям в Lua-хранимках нужны сокеты, http клиенты и прочее :)


          1. rtsisyk
            10.11.2015 09:37

            Реплицировать вызовы Lua-функции на slave идеологически не правильно. Подобного рода кактусы уже грызли в MySQL со statement-based replication. В Тарантуле мы реплицируем binary log примитивных запросов (insert, replace, update, delete), поэтому никаких проблем с рандомом и прочим просто не существует. Репликация вообще ничего не знает про Lua, сишные хранимки и т.п., т.к. работает на уровне запросов базы данных.


            1. AxVPast
              10.11.2015 15:26

              Все зависит от того, как Вы испольуете инструмент. В том виде в которм мне его приходится использовать — репликация именно вызовов — очень удачная идея. Дело в том, что 1 такой вызов может за собой скрывать миллионы изменений. Отличный пример «не получившегося scan + hdel» у misterion. Там речь шла о миллионах удаленных ключей при помощи 1 вызова. Я тоже как-то на досуге думал почему бы не сделать lua скрипты транзакционными. Ведь достаточно просто скешировать все изменения которые они делают, а потом, по окончании работы скрипта, их накатить на реальную базу (можно еще сделать один корень куда сохранять изменения"). После чего пришел к выводу, что такое решение потребует 2х кратный объем памяти относительно текущего размера БД. И если это 200ГБ то потребуется еще 200ГБ (или еще больше, если lua скрипт в себя «всосет» практически всю базу).


  1. zloidemon
    09.11.2015 14:23

    Дикий ад, не проще ли Tarantool поставить и не мучить себя?


  1. rtsisyk
    09.11.2015 14:53

    Алексей, советую Вам обратить внимание на Tarantool по следующим причинам:

    1. В Tarantool Lua — first class citizen. Есть интерактивная консоль, можно запускать скрипты с #!/usr/bin/tarantool, из Lua кода есть доступ абсолютно ко всем функциям Tarantool. Вкупе это дает более прозрачный и удобный цикл отладки и разработки.

    2. Нет проблем с типизацией, т.к. хранилище внутри и так уже использует MsgPack

    Кроме того, уже есть множество удобных библиотек, начиная от сокетов, заканчивая http сервером.

    // Disclaimer: разработчик Tarantool


    1. alexeyknyshev
      09.11.2015 15:09

      Спасибо, уже начал поверхностное изучение.


  1. AlexeiZhuravlev
    10.11.2015 11:49

    Хочу задать косвенный вопрос комментирующим. Кто-нибудь смотрел на замену Redis в виде SSDB и Ledis? (Обертки над различными базами, например leveldb, понимающие на 90% протокол Redis) Когда редисом хочется пользоваться, но память на сервере не позволяет.

    Есть какие-то впечатления по ним из опыта?


    1. aleks_raiden
      10.11.2015 23:20

      я использовал SSDB полтора года на продакшине, где-то 4 миллиарда ключей, 38Гб места, скорость была очень хорошей. Проблему создавало только обслуживание, например compact на такой базе требовал х2 места. В остальном вроде никаких проблем не было