Redis支持在LUA脚本中使用任意精度。

5
我需要能够在 Redis 中进行以下事务的操作:
  • 只有当结果>0时,才将n值减少
  • 否则什么也不做
  • 处理任意精度的小数(我需要它们以浮点格式)
  • 可被其他进程访问
简单来说,这是一个“余额”:如果我在此字段中有足够的余额,我就可以使用它,否则就不能。有时候,它必须减少多个余额。
为了实现这一点,我编写了一个 LUA 脚本,计算减量的结果,然后用这个结果修改字段。我选择这种解决方案,因为: 我遇到的问题如下:
  • 使用的库不符合要求:它仅适用于整数,并且每次发送时太大(即使使用evalsha也很慢)
  • 在Redis中编写Lua脚本时如何包含第三方库 => 遵循此方法后,我对Redis上使用附加模块的用法感到困惑。不过,这是从过去看的。现在情况怎么样?
  • 是否有更有效的方法来做到这一点?欢迎对代码本身提出建议
  • Redis是否真正能够满足我的需求?
输入“值”格式如下:Array<{key: string, field: string, value: string // 实际上这是一个以字符串格式表示的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

Redis的Lua仍然没有外部包... 我会研究一下模块或者redisgears。 - Itamar Haber
如果你真的需要任意精度的数字,那么你不能使用浮点数,而Redis可能不是一个好的选择。(除非你自己编写模块,就像Itamar提到的那样。) - Kevin Christopher Henry
2个回答

5

正如您在回答中所指出的那样,编写自己的模块可能是一个非常适合您要求的选项。

这样的模块将使用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.h
  • 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
也许受事件溯源模式的启发可以解决您的问题。另一种实现原子性的方法是将写入角色限制为仅一个处理器,其命令始终按时间顺序排列(就像使用lua的redis一样)。
1)您向Redis发送存储在排序集中的余额更改“事件”(用于时间排序,时间戳为分数)。仅存储要执行的“命令”(而不是计算结果)。例如,“-1.545466”,“+2.07896”等。
2)然后,您通过Lua脚本从单个处理器消耗这些事件(必须确保只有一个计算项访问此数据,否则会遇到麻烦),可以使用调用每n秒一次脚本的循环来调用它(您可以定义您的实时质量),例如Apache Storm(一个“spout”)。 脚本应返回从最旧的时间戳到最新的时间戳的事件,时间戳(分数)也应返回(如果没有它们,您将失去“索引”),当然还有实际余额。
您应该获得类似以下的值:
balance= +5
ZSET=
"-6" score 1557782182
"+2" score 1557782772
"+3" score 1678787878

3)在您的中间件服务器(唯一一个允许修改余额的服务器)中,您可以使用任何库/技术来计算余额的变化(应该非常快速)。每次只需遍历事件以计算余额。请注意,由于此操作,您将减少对Redis的更改。

您应该获得以下结果:

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

4) 一旦您在服务器中计算出新的余额值,就是时候通过Lua脚本将结果和使用的事件发送到redis以进行:

  • 更新余额值,因为只允许一个进程修改它,所以不应该出现任何交易问题,它也应该始终正确地按时间排序
  • 修剪已计算事件的排序集(步骤2中使用的最旧和最新时间戳将用于此),以便这些事件在下一次lua调用中不会被再次处理

5) 获利。

请注意,在调用另一个操作2之前,操作4应该完成,您可以在redis中设置一个旧的信号量项来防止这种情况(“busy”键可防止操作2在操作4未完成时运行,当启动步骤2时设置它,完成步骤4时清除它,还可以对其进行驱逐,以便如果发生故障,则驱逐将作为超时工作以启动另一次迭代)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接