Redis поддерживает произвольную точность в скриптах LUA - PullRequest
5 голосов
/ 26 апреля 2019

Мне нужно иметь возможность сделать транзакцию в Redis, которая выполняет следующие действия:

  • уменьшить значение n, если и только если результат> 0
  • иначе ничего не делать
  • иметь дело с десятичными числами произвольной точности (они мне нужны в формате с плавающей запятой)
  • быть доступным для других процессов

Проще говоря, это «Баланс»: если мне хватит в этой области, я могу использовать его, иначе нет. Когда-нибудь это должно уменьшить многие балансы

Для этого я создал скрипт LUA, который вычисляет результат декрементации, а затем модифицирует поля с этим результатом. Я выбрал это решение, потому что:

  • это атомно
  • более простой INCRBYFLOAT выполняет вычитание независимо от результата и, похоже, не имеет правильной точности
  • Использовал библиотеку LUA http://oss.digirati.com.br/luabignum/

Проблемы, с которыми я сталкиваюсь:

  • Используемая библиотека не подходит: она только для целых чисел, и она слишком велика для отправки каждый раз (событие с evalsha, это медленно)
  • Как включить стороннюю библиотеку при программировании Lua-скрипта в Redis => После этого я довольно застрял в использовании дополнительных модулей в Redis. Тем не менее, это из прошлого, сейчас. Как дела?
  • Я не уверен, есть ли более эффективный способ сделать это? Любые советы по самому коду приветствуются
  • Redis действительно способ удовлетворить мои потребности?

Входные данные «values» имеют следующий формат: Array <{ключ: строка, поле: строка, значение: строка // это на самом деле BigNumber со строковым форматом}>

this.redisClient.eval(`
    ${luaBigNumbers}

    local operations = cjson.decode(KEYS[1])
    local isStillValid = true
    local test

    for k, v in pairs(operations) do
      local temp = BigNum.new(redis.call('hget', v.key, v.field))
      local res = BigNum.mt.add(temp, BigNum.new(v.value))

      if BigNum.mt.lt(res, BigNum.new('0')) then
        isStillValid = false
      end
    end

    if isStillValid then
      for k, v in pairs(operations) do
        local temp = BigNum.new(redis.call('hget',v.key, v.field))
        redis.call('hset', v.key, v.field, BigNum.mt.tostring(BigNum.mt.add(temp, BigNum.new(v.value))))
      end
    end

    return tostring(isStillValid)`,
  1, JSON.stringify(values), (err, reply) => {

TL; DR: мне нужно иметь функцию общего баланса в Redis, как это сделать?

Опубликовано в обмене стека, если у вас есть идея, как его реализовать https://softwareengineering.stackexchange.com/questions/391529/what-architecture-is-the-most-adapted-for-a-shared-balance-in-nodejs-and-maybe

Ответы [ 2 ]

3 голосов
/ 14 мая 2019

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

Такой модуль был бы написан на C. Следовательно, необходима десятичная библиотека, которая соответствует математическим требованиям финансовых приложений.

Здесь я использую библиотеку decNumber C, библиотеку, изначально написанную IBM. Я использовал для своего теста следующие ссылки:

Демо

Перед просмотром кода здесь небольшая демонстрация:

balance decrement demo

Как видите, он работает с произвольной точностью.

Команда, подобная balance.decrement mykey myfield "0.1", уменьшает mykey myfield на значение, переданное в качестве последнего строкового параметра. Новое значение сохраняется в mykey myfield и выводится как результат команды. Если результат будет меньше 0, он не уменьшается. Затем выводится NOP. Операция атомная.

Модуль источника

#include "../redismodule.h"
#include "../rmutil/util.h"
#include "../rmutil/strings.h"
#include "../rmutil/test_util.h"

#define  DECNUMDIGITS 34

#include "decNumber.h"


int decrementCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {

    if (argc != 4) {
        return RedisModule_WrongArity(ctx);
    }
    RedisModule_AutoMemory(ctx);

    RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_READ | REDISMODULE_WRITE);
    if (RedisModule_KeyType(key) != REDISMODULE_KEYTYPE_HASH &&
        RedisModule_KeyType(key) != REDISMODULE_KEYTYPE_EMPTY) {
        return RedisModule_ReplyWithError(ctx, REDISMODULE_ERRORMSG_WRONGTYPE);
    }

    RedisModuleCallReply *currentValueReply = RedisModule_Call(ctx, "HGET", "ss", argv[1], argv[2]);
    RMUTIL_ASSERT_NOERROR(ctx, currentValueReply);

    RedisModuleString *currentValueRedisString = RedisModule_CreateStringFromCallReply(currentValueReply);
    if (!currentValueRedisString) {
        return 0;
    }
    const char *currentValueString = RedisModule_StringPtrLen(currentValueRedisString, NULL);
    const char *decrementValueString = RedisModule_StringPtrLen(argv[3], NULL);

    decNumber currentNum, decrementNum;
    decContext set;
    char resultStr[DECNUMDIGITS + 14];
    decContextDefault(&set, DEC_INIT_BASE);
    set.traps = 0;
    set.digits = DECNUMDIGITS;

    decNumberFromString(&currentNum, currentValueString, &set);
    decNumberFromString(&decrementNum, decrementValueString, &set);

    decNumber resultNum;
    decNumberSubtract(&resultNum, &currentNum, &decrementNum, &set);

    if (!decNumberIsNegative(&resultNum)) {
        decNumberToString(&resultNum, resultStr);
        RedisModuleCallReply *srep = RedisModule_Call(ctx, "HSET", "ssc", argv[1], argv[2], resultStr);
        RMUTIL_ASSERT_NOERROR(ctx, srep);

        RedisModule_ReplyWithStringBuffer(ctx, resultStr, strlen(resultStr));
        return REDISMODULE_OK;
    }

    if (RedisModule_CallReplyType(currentValueReply) == REDISMODULE_REPLY_NULL) {
        RedisModule_ReplyWithNull(ctx);
        return REDISMODULE_OK;
    }

    RedisModule_ReplyWithSimpleString(ctx, "NOP");
    return REDISMODULE_OK;
}


int RedisModule_OnLoad(RedisModuleCtx *ctx) {
    if (RedisModule_Init(ctx, "balance", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR) {
        return REDISMODULE_ERR;
    }
    RMUtil_RegisterWriteCmd(ctx, "balance.decrement", decrementCommand);
    return REDISMODULE_OK;
}

Как собрать и запустить

Я бы порекомендовал клонировать https://github.com/RedisLabs/RedisModulesSDK. Есть пример папки Замените module.c указанным выше кодом модуля. Скопируйте следующие файлы из библиотеки decNumber C в папку примера:

  • decContext.c
  • decNumber.h
  • decNumber.c
  • decNumberLocal.h

Измените Makefile в папке примера, чтобы строка, начинающаяся с module.so, выглядела так:

module.so: module.o decNumber.o decContext.o
    $(LD) -o $@ module.o decNumber.o decContext.o $(SHOBJ_LDFLAGS) $(LIBS) -L$(RMUTIL_LIBDIR) -lrmutil -lc 

Введите эти команды в базовом каталоге:

make clean
make

Вы можете проверить это тогда:

redis-server --loadmodule ./module.so

Это то, что вы ищете?

1 голос
/ 14 мая 2019

Может быть, вдохновиться паттерном поиска событий - это то, что может решить вашу проблему.Также еще один способ достижения атомарности - ограничить роль записи только одним процессором, команды которого всегда будут упорядочены по времени.(так же, как redis с lua)

1) Вы отправляете в redis «события» изменения баланса, хранящиеся в отсортированном наборе (для упорядочивания по времени, отметка времени является счетом).Сохраняйте только ту «команду», которую вы хотите выполнить (а не результат вычислений).Например, «-1.545466», «+2.07896» и т. Д. *

2) Затем вы используете эти события с помощью сценария Lua от одного процессора (вы должны быть уверены, что существует только один вычислительный элемент, который обращается кэти данные или у вас будут проблемы), которые можно вызывать с помощью цикла, который вызывает скрипт каждые n секунд (вы можете определить качество в реальном времени), например, в виде Apache Storm («носик»).Сценарий должен возвращать события от самой старой отметки времени до самой последней отметки времени, также должны быть возвращены отметки времени (оценки) (без них вы потеряете «индекс») и, конечно, фактический баланс.

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

balance= +5
ZSET=
"-6" score 1557782182
"+2" score 1557782772
"+3" score 1678787878

3) На своем сервере промежуточного программного обеспечения (уникальный, единственный, которому разрешено изменять баланс) вы вычисляете изменения в балансе (используя любую библиотеку lib / tech, которую выхочу на вашем сервере, должно быть молниеносно).Вы просто перебираете события, чтобы каждый раз вычислять баланс.Обратите внимание, что вы будете делать меньше мутаций в redis благодаря этому.

Вы должны получить результат

old_balance=5
new_balance=10
ZSET=
"-6" score 1557782182
"+2" score 1557782772
"+3" score 1678787878

4) Как только вы вычислили новое значение баланса на вашем сервере, пришло времячтобы отправить результат и события, которые вы использовали для повторного отображения, с помощью сценария Lua для:

  • обновления значения баланса, поскольку только один процесс может изменять его, вы не должны получать никаких проблем с транзакциями, он должентакже всегда будет правильно упорядочен по времени
  • , обрезая отсортированный набор вычисленных событий (для этого будут использоваться самые старые и самые последние временные метки, использованные на шаге 2), чтобы эти события не обрабатывались снова при следующем вызове lua

5) Прибыль.

Обратите внимание, что операция 4 должна быть завершена до вызова другой операции 2, вы можете установить старый семафор, подобный элемент в redis, чтобы предотвратить эту клавишу («занят»)который запрещает запуск операции 2, если операция 4 не завершена, вы устанавливаете его при запуске шага 2, очищаете его по завершении шага 4, вы также можете установить выселение oВ этом случае, если что-то пойдет не так, выселение будет работать как тайм-аут для начала следующей итерации).

...