Redis 支持 LUA 脚本中的任意精度

Redis support arbitrary precision in LUA Scripts

我需要能够在 redis 中执行以下操作:

简单点说,就是一个"Balance":如果我这方面够用,我就用,否则不行。有时,它必须减少许多余额

为此,我制作了一个 LUA 脚本来计算递减的结果,然后用这个结果修改字段。我选择了这个解决方案,因为:

我面临的问题:

输入,"values"格式如下: 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

也许从事件溯源模式中获得灵感可以解决您的问题。实现原子性的另一种方法是将写入角色限制为只有一个处理器,其命令将始终按时间排序。 (就像带有 lua 的 redis)

1) 您将存储在排序集中的余额变化 "events" 发送到 Redis(对于时间排序,时间戳是分数)。只存储你想做的"command"(不是计算的结果)。例如“-1.545466”、“+2.07896”等...

2) 然后你通过单个处理器的 Lua 脚本使用这些事件(你必须确保只有一个计算项目访问这些数据,否则你会遇到麻烦)这可以是使用每 n 秒调用一次脚本的循环调用(您可以定义您的实时质量),例如 Apache Storm("spout")。 该脚本应该 return 从最早的时间戳到最新的时间戳的事件,时间戳(分数)也应该 returned(没有它们你会丢失 "index")当然还有实际余额

您应该得到如下所示的值:

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 的事件了:

  • 更新余额值,因为只允许一个进程修改它,你不应该遇到任何交易问题,它也应该总是time-ordered正确
  • 修剪已排序的计算事件集(将使用第 2 步中使用的最旧和最新时间​​戳),以便在下一次 lua 调用时不会再次处理这些事件

5) 利润。

请注意,操作 4 应该在另一个操作 2 被调用之前完成,您可以在 redis 中设置一个旧的信号量,如 item 以防止("busy" 阻止操作 2 的键为 运行 如果操作 4 未完成,您在启动第 2 步时设置它,在第 4 步完成时清理它,您还可以对其设置驱逐,这样如果出现问题,驱逐将作为另一个迭代开始的超时).

如您回答的评论中所述,编写您自己的模块将是一个非常适合您要求的选项。

这样的模块将用C 编写。因此需要满足金融应用程序数学要求的十进制库。

这里我用的是decNumber C库,IBM原先写的一个库。我使用以下链接进行测试:

演示

在查看这里的代码之前,先看一个小演示:

如您所见,它可以任意精度工作。

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

这是你要找的吗?