Глава 11. Расширение Redis при помощи модулей Redis

В данной главе мы рассмотрим следующие рецепты:

  • Загрузка модуля Redis.

  • Написание модуля Redis.

Введение

Хотя Redis и поддерживает приличное число типов данных и великолепные функции, порой мы можем пожелать добавить в Redis наши собственные индивидуальные типы данных или команды. В Главе 3, Свойства данных мы изучили как для реализации нашей собственной индивидуальной логики поверх встроенных типов данных и команд Redis могут применяться сценарии Lua. Кроме того, начиная с Redis 4.0 мы можем расширять возможности Redis с помощью модулей Redis.

Модули Redis являются совместно используемыми библиотеками C, которые могут загружаться вашим сервером Redis при запуске или в процессе исполнения. Модули Redis по сравнению со сценариями Lua имеют следующие преимущества:

  • Будучи библиотеками C, модули Redis исполняются намного быстрее чем сценарии Lua.

  • В модулях Redis могут создаваться новые типы данных, в то время как в сценариях Lua могут применяться только имеющиеся типы данных.

  • Создаваемые в модулях C libraries, Redis команды могут вызываться из некоторого клиента напрямую, как если бы они были естественными командами, в то время как сценарии Lua обязаны вызываться через EVAL или EVALSHA.

  • В модули Redis можно привязывать библиотеки сторонних производителей, в то время как то, что мы можем делать в сценариях Lua очень ограничено.

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

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

Загрузка модуля Redis

Для расширения функциональности Redis может применяться множество модулей Redis с открытым исходным кодом. В данном рецепте мы рассмотрим как загружать некий модуль в Сервер Redis. Мы загрузим модуль ReJSON, который добавит тип данных JSON для поддержки в Redis.

Подготовка...

Вам следует завершить установку своего Сервера Redis как мы объясняли в своём рецепте Загрузка и установка Redis в Главе 1, Приступая к Redis.

Как это сделать...

Шаги загрузки некоторого модуля Redis таковы:

  1. С GitHub выгрузите необходимый исходный код ReJSON:

    
    $ git clone https://github.com/RedisLabsModules/rejson.git
    Cloning into 'rejson'...
    remote: Counting objects: 2066, done.
    remote: Compressing objects: 100% (64/64), done.
    remote: Total 2066 (delta 52), reused 83 (delta 43), pack-reused
    1952
    Receiving objects: 100% (2066/2066), 3.68 MiB | 0 bytes/s, done.
    Resolving deltas: 100% (1114/1114), done.
    Checking connectivity... done.
     	   
  2. Перейдите в каталог требующегося вам модуля и скомпилируйте этот модуль, запустив make:

    
    $ cd rejson/src
    ~/rejson/src$ make
     	   

    Получаемый двоичный файл модуля будет создан в виде rejson.so:

    
    ~/rejson/src$ ls -l rejson.so
    -rwxrwxr-x 1 user user 455888 Dec 29 17:56 rejson.so
     	   
  3. Для загузки модуля из redis-cli примените команду MODULE LOAD с соответствующим путём к вашему двоичному модулю:

    
    127.0.0.1:6379> MODULE LOAD /redis/rejson/src/rejson.so
    OK
     	   
  4. Убедитесь что необходимый модуль загружен:

    
    127.0.0.1:6379> JSON.SET jsonKey . '{"foo": "bar", "baz": ["aaa", "bbb"]}'
    OK
    127.0.0.1:6379> JSON.GET jsonKey
    "{\"foo\":\"bar\",\"baz\":[\"aaa\",\"bbb\"]}"
     	   
  5. Для выгрузки модуля воспользуйтесь имеющейся командой MODULE UNLOAD с необходимым названием модуля:

    
    127.0.0.1:6379> MODULE UNLOAD ReJSON
    (error) ERR Error unloading module: the module exports one or more module-side data types, can't unload
     	   
  6. Для перечисления всех загруженных модулей воспользуйтесь MODULE LIST:

    
    127.0.0.1:6379> MODULE LIST
    1) 1) "name"
       2) "ReJSON"
       3) "ver"
       4) (integer) 10001
     	   
  7. при помощи директивы loadmodule, модули Redis также можно загружать в файле настроек:

    
    loadmodule /redis/rejson/src/rejson.so
     	   

Как это работает...

Загрузка некоторого модуля Redis достаточно проста; после того как модуль был успешно загружен, мы также можем увидеть это из журналов Сервера Redis:


1960:M 29 Dec 18:31:07.403 # <ReJSON> JSON data type for Redis v1.0.1 [encver 0]
1960:M 29 Dec 18:31:07.403 * Module 'ReJSON' loaded from /redis/rejson/src/rejson.so
 	   

Имеющаяся в вашем распоряжении команда MODULE UNLOAD принимает собственно название модуля вместо пути к файлу библиотеки этого модуля, поэтому хорошим практическим приёмом является оставлять имя файла библиотеки определённого модуля тем же самым, что и само название модуля.

В нашем предыдущем примере, так как наш модуль ReJSON создал некий новый тип данных, он не моет быть выгружен при исполнении сервера. Если некий модуль был загружен при помощи команды MODULE LOAD, он будет выгружен автоматически после остановки данного сервера.

Дополнительно

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

Хаб модулей Redis.

С подробностями модулей Redis вы можете ознакомиться во введении в модули Redis/

Создание модуля Redis

Redis предоставляет пользователям широкие возможности API C для построения модулей. В то время, как модули Redis могут создаваться на любом языке, который обладает функциональностью C, написание модулей Redis на C является самым простым и непосредственным способом.

В данном рецепте мы собираемся реализовать некий модуль Redia, MYMODULE, на языке C при помощи новой команды ZIP, которая может быть вызвана следующим образом:


MYMODULE.ZIP destination_hash field_list value_list
		

Данная команда получает два списка Redis и пытается создать некий хэш Redis, чьими полями являются элементы в field_list и соответствующими значениями являются элементы с тем же самым индексом в value_list.

Например, если в качестве field_list выступает ["john", "alex", "tom"], а value_list составляет ["20", "30", "40"], destination_hash составит ["20", "30", "40"], destination_hash будут {"john": "20", "alex": "30", "tom": 40}. Если значения длины двух этих списков не будут равны, более длинный перечень будет усечён до той же самой длины, которую имеет более короткий прежде чем будет создан необходимый хэш (сам по себе перечень не будет изменён после этого). Если в field_list имеются дублируемые элементы, в значениях хэширования будут сохранены соответствующие значения самых правых индексов Соответствующая команда вернйт значение длины создаваемого хэша.

Подготовка...

Вам следует завершить установку своего Сервера Redis как мы объясняли в своём рецепте Загрузка и установка Redis в Главе 1, Приступая к Redis.

Как это сделать...

Основные этапы создания модуля Redis состоят в следующем:

  1. Создайте некий файл mymodule.c и сделайте включение соответствующего файла заголовка, redismodule.h:

    
    #include "redismodule.h"
     	   
  2. Создайте некую функцию RedisModule_OnLoad():

    
    int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
      if (RedisModule_Init(ctx, "mymodule", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR) {
        return REDISMODULE_ERR;
      } 
    
      if (RedisModule_CreateCommand(ctx, "mymodule.zip", MyModule_Zip, "write deny-oom no-cluster", 1, 1, 1) == REDISMODULE_ERR) {
        return REDISMODULE_ERR;
      }
    
      return REDISMODULE_OK;
    }
     	   
  3. Создайте некую функцию MyModule_Zip(), которая является функцией обработчиком нашей новой команды:

    
    int MyModule_Zip(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
      // MYMODULE.ZIP destination field_list value_list
      if (argc != 4) {
        return RedisModule_WrongArity(ctx);
      }
      RedisModule_AutoMemory(ctx);
    
      // Open field/value list keys
      RedisModuleKey *fieldListKey = RedisModule_OpenKey(ctx, argv[2], REDISMODULE_READ | REDISMODULE_WRITE);
      RedisModuleKey *valueListKey = RedisModule_OpenKey(ctx, argv[3], REDISMODULE_READ | REDISMODULE_WRITE);
      if ((RedisModule_KeyType(fieldListKey) != REDISMODULE_KEYTYPE_LIST && RedisModule_KeyType(fieldListKey) != REDISMODULE_KEYTYPE_EMPTY) || (RedisModule_KeyType(valueListKey) != REDISMODULE_KEYTYPE_LIST && RedisModule_KeyType(valueListKey) != REDISMODULE_KEYTYPE_EMPTY)) {
        return RedisModule_ReplyWithError(ctx, REDISMODULE_ERRORMSG_WRONGTYPE);
      }
    
      // Open destination key
      RedisModuleKey *destinationKey = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE);
      RedisModule_DeleteKey(destinationKey);
    
      //Get length of lists
      size_t fieldListLen = RedisModule_ValueLength(fieldListKey);
      size_t valueListLen = RedisModule_ValueLength(valueListKey);
    
      if (fieldListLen == 0 || valueListLen == 0) {
        RedisModule_ReplyWithLongLong(ctx, 0L);
       return REDISMODULE_OK;
      }
    
      size_t fCount = 0;
      size_t vCount = 0;
      while (fCount < fieldListLen && vCount < valueListLen) {
        //Pop from left and push back to right
        RedisModuleString *key = ListLPopRPush(fieldListKey);
        RedisModuleString *value = ListLPopRPush(valueListKey);
        //Set hash
        RedisModule_HashSet(destinationKey, REDISMODULE_HASH_NONE, key, value, NULL);
        fCount++;
        vCount++;
      }
    
      while (fCount++ < fieldListLen) {
        ListLPopRPush(fieldListKey);
      }
    
      while (vCount++ < valueListLen) {
        ListLPopRPush(valueListKey);
      }
    
      //Get hash length
      RedisModuleCallReply *hlenReply = RedisModule_Call(ctx, "HLEN", "s", argv[1]);
      if (hlenReply == NULL) {
        return RedisModule_ReplyWithError(ctx, "Failed to call HLEN");
      } else if (RedisModule_CallReplyType(hlenReply) == REDISMODULE_REPLY_ERROR) {
        RedisModule_ReplyWithCallReply(ctx, hlenReply);
        return REDISMODULE_ERR;
      }
    
      RedisModule_ReplyWithLongLong(ctx, RedisModule_CallReplyInteger(hlenReply));
    
      RedisModule_ReplicateVerbatim(ctx);
    
      return REDISMODULE_OK;
    }
     	   
  4. Создайте некую функцию ListLPopRPush() для чтения элементов из списка. ListLPopRPush() и MyModule_Zip() следует разместить перед RedisModule_OnLoad(), либо их следует объявить в самом начале данного файла:

    
    static RedisModuleString *ListLPopRPush(RedisModuleKey *ListKey) {RedisModuleString *value = RedisModule_ListPop(ListKey, REDISMODULE_LIST_HEAD);
      RedisModule_ListPush(ListKey, REDISMODULE_LIST_TAIL, value);
      return value;
    }
     	   
  5. Скопируйте redismodule.h из соответствующего пакета исходного кода Redis в тот же самый каталог redismodule.h и скомпилируйте его при помощи gcc:

    
    gcc -fPIC -shared -std=gnu99 -o mymodule.so mymodule.c
     	   
  6. Теперь скопируйте этот полученный в результате файл библиотеки модуля в удобное местоположение (например, /redis) и загрузите наш модуль в Redis:

    
    127.0.0.1:6379> MODULE LOAD /redis/mymodule.so
    OK
     	   
  7. Давайте испытаем наши новые команды:

    
    127.0.0.1:6379> RPUSH fields john alex tom
    (integer) 3
    127.0.0.1:6379> RPUSH values 20 30 40
    (integer) 4
    127.0.0.1:6379> MYMODULE.ZIP myhash fields values
    (integer) 3
    127.0.0.1:6379> HGETALL myhash
    1) "john"
    2) "20"
    3) "alex"
    4) "30"
    5) "tom"
    6) "40"
     	   

Как это работает...

Функция RedisModule_OnLoad() вызывается при загрузке модуля Redis. Она обязана быть реализована во всех модулях Redis, тмак как она является необходимой точкой входа. Обычно нам требуется инициализировать конкретный модуль вначале при помощи соответствующего API RedisModule_Init() и определить необходимые название модуля, его версию и версию API. Модули Redis проектируются как равнодушные к версиям Сервера Redis, тем не менее они должны соотноситься с версиями API. А именно, некий модуль не следует повторно компилировать чтобы загружать его в различных версиях Сервера Redis, до тех пор, пока данный сервер знает какие именно версии API применяет данный модуль. Текущей версией API модуля Redis является 1 ((REDISMODULE_APIVER_1)). Нам также требуется создавать команлы для этого модуля, что осуществляется при помощи функции RedisModule_CreateCommand(), в которую мы передаём название соответствующей команды, необходимый указатель на обработчик этой команды, а также некую строку флагов, которые определяют соответствующее поведение данной команды. В нашей команде MYMODULE.ZIP соответствующий обработчик команды реализуется в функции MyModule_Zip(). Данная команда может быть изменять некий набор данных Redis и запрашивать новое пространство памяти, которое может претерпеть Out Of Memory (OOM); это не поддерживается кластерами Redis. Полный перечень флагов можно отыскать в соответствующем справочном руководстве API. Самые последние три параметра RedisModule_CreateCommand() обозначают первый индекс ключа, последний индекс ключа и шаг ключа в данной команде. Так как наша команда имеет только один ключ (для хеширования назначения) мы передаём только (1, 1, 1). Если вы создаёте некую команду наподобии MSET (MSET key value [key value]...), следует применять (1, -1, 1), поскольку MSET получает неограниченные ключи (самый последний индекс = -1). Возвращаемым значением лолжно быть REDISMODULE_OK, если нет никаких ошибок.

Соответствующая функция обработки команд MyModule_Zip() получает три параметра- контекст модуля, вектор параметров команды и общее число параметров команды. Как вы можете заметить, почти все API модуля Redis получают значение контекста соответствующего модуля в качестве аргумента. Данный контекст применяется для получения ссылки на сам это модуль, а также на его команды, клиентов, которые вызывали команды и тому подобного. Данный контекст передаётся иным модулем API и его вектор аргументов будет передан тем клиентом, который вызвал данную команду.

Внутри данной функции, прежде всего, мы проверяем правильность общего числа команд. Так как данная команда спроектирована для получения трёх параметров (destination_hash, field_list и value_list), включая саму команду, общим числом параметров команды должно быть (4. Затем мы просим Redis автоматически управлять имеющимся ресурсом и памятью в нашем обработчике команды. Это осуществляется посредством простого вызова ( RedisModule_AutoMemory(ctx).

Затем мы запускаем ключи доступа Redis, которыми, в нашем примере, являются два списка Redis. Redis предоставляет два набора API для модулей Redis с целью доступа к пространству данных Redis. Такой API из двух уровней может осуществлять доступ к ключам Redis и очень быстро манипулировать структурами данных. Самый верхний уровень API позволяет имеющемуся коду API вызывать команды Redis и осуществлять выборку получаемых результатов, что очень похоже на то, как сценарии Lua получают доступ к пространству данных Redis. В нашем примере мы используем API нижнего уровня (RedisModule_OpenKey() для открытия двух перечней входа и соответствующего хэша получателя. Данный API возвращает некий указатель в (RedisModuleKey, который является обработчиком для соответствующего ключа Redis. Затем мы проверяем полученный тип ключа при помощи (RedisModule_KeyType() чтобы убедиться является ли данный входной параметр списком или он пуст.

Наш следующий шаг состоит из прохода циклом по двум спискам и назначения значений нашему хэшу. В нашем случае мы создаём функцию помощника ListLPopRPush() для чтения элементов из некоторого списка. Эта функция выталкивает элемент с левого конца и затем помещает его обратно в правой тороне данного списка, возвращая соответствующее значение эелемента вызывающей стороне. Мы применяем RedisModule_ValueLength() и RedisModule_HashSet() для получения общей длины каждого списка, но нам всё ещё необходимо выталкивать и продвигать все остальные элементы из всех прочих списков, которые могут быть более длинными с тем, чтобы оставлять эти два списка неизменными.

Так как всем командам требуется возвращать общую длину создаваемого ими кэша, в данном случае мы применяем edisModule_Call() для вызова команды HLEN. RedisModule_Call() может применяться для вызова любой команды Redis и её результат будет возвращён в качестве некоего указателя в RedisModuleCallReply.RedisModule_CallReplyInteger() RedisModuleCallReply. RedisModule_CallReplyInteger() возвращает полученное значение результата в виде целого, которое затем передаётся в RedisModule_ReplyWithLongLong() и вызывается вызывавшей стороне нашей команды.

RedisModule_Call() получает переменную длину параметров. Самый первый параметр является контекстом данного модуля, а второй параметр является завершаемым нулём строковым параметром C с соответствующим именем команды. Третьим параметром является определитель формата для необходимых аргументов команды, поскольку данные параметры могут появляться различными способами в виде строк C с нулевым символом в конце, объектов RedisModuleString, получаемых из параметра argv в реализации данной команды, а также тому подобного. В нашем примере значение аргумента для HLEN получается из RedisModuleString как получаемое в argv, поэтому мы просто помещаем s в соответствующий определитель формата. Все остальные значения параметров являются аргументами соответствующей команды.

Вот полный перечень определителей формата:

  • c --: Завершаемый нулём указатель строки C.

  • b --: Буфер C, указатель строк с необходимыми двумя параметрами, а также длина size_t.

  • s --: RedisModuleString в виде получаемой в argv или прочих API модуля Redis, которые возвращают некий объект RedisModuleString.

  • l --: Длинное целое значение.

  • v --: Массив объектов RedisModuleString.

  • ! --: Этот модификатор всего лишь просит соответствующую функцию выполнить репликацию данной команды в подчинённых и AOF. Он игнорируется с точки зрения разбора параметров.

RedisModule_ReplicateVerbatim(ctx) означает дословную передачу нашей команды в реплики Redis.

Например, если мы исполняем значение поля MYMODULE.ZIP myhash- в некотором хозяине Redis, его подчинённые получат ту же самую команду повторяемую дословно.

Также ознакомьтесь...

Вы могли заметить, что RedisModule_OnLoad() также получал некий векторный аргумент и общее число параметров. Они являлись параметрами для данного модуля, которые могут определяться при загрузке такого модуля, например, MODULE LOAD mymodule arg1 arg2 arg3. Это полезно в том случае, когда вы желаете чтобы ваш модуль вёл себя различным способом при передаче ему различных параметров.

Дополнительно

Чтобы получить всю справочную информацию по API модулей Redis, проверьте https://redis.io/topics/modules-api-ref.

Официальное введение в API модулей Redis.

Как писать модули API от Двир Волк

Имеется библиотека с названием RedisModuleSDK, которая добавляет пару функций применения и макросов для разработки модулей Redis: https://github.com/RedisLabs/RedisModulesSDK.