Redis support arbitrary precision in LUA Scripts
Asked Answered
O

2

5

I need to be able to make a transaction in redis that does the following:

  • decrement n value if and only if the result is > 0
  • otherwise, do nothing
  • deal with arbitrary precision decimal numbers (I need them in a float format)
  • be accessible to other processes

Simpler put, it's a "Balance": If I have enough in this field, I can use it, otherwise, no. Sometime, it must decrement many balances

To do this, I made a LUA Script that calculates the result of the decrementation, then modifies the fields with this result. I chose this solution, because:

The problems I'm facing:

  • The lib used doesn't fit: It's only for integers, and it's too big to send each time (event with evalsha, it's slow)
  • How to include third party library when programming Lua script in Redis => following that, I'm pretty stuck concerning the usage of additionnal modules on redis. However, it's from the past, now. How are things now ?
  • I'm not event sure if there is a more efficient way to do that ? Any advices on the code itself are welcomed
  • Is Redis really a way to fullfill my needs ?

The input, "values" is the following format: Array<{ key: string, field: string, value: string // this is actually a BigNumber, with a string format }>

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: I need to have a shared balance function on Redis, how to do that well ?

Posted in stack exchange if you have an idea of how to implement it https://softwareengineering.stackexchange.com/questions/391529/what-architecture-is-the-most-adapted-for-a-shared-balance-in-nodejs-and-maybe

Osteoplastic answered 26/4, 2019 at 17:35 Comment(2)
Still no external packages in Redis' Lua... I'd look into modules or perhaps redisgearsUncommunicative
If you really need numbers with arbitrary precision then you can't use floats, and Redis may not be a good choice. (Unless you write your own module, as Itamar mentioned.)Electromotive
B
5

As indicated in the comments to your answer, writing your own module would be an option that could fit your requirements very well.

Such a module would be written in C. Hence a decimal library that meets the mathematical requirements of financial applications is needed.

Here I use the decNumber C library, a library written originally by IBM. I used for my test the following links:

Demo

Before looking at the code here a small demo:

balance decrement demo

As you can see, it works with arbitrary precision.

A command like balance.decrement mykey myfield "0.1" decrements mykey myfield with the value passed as the last string parameter. The new value is stored in mykey myfield and output as the result of the command. If the result would be less than 0, it is not decremented. Then a NOP is output. The operation is atomic.

Module Source

#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;
}

How To Build and Run

I would recommend cloning https://github.com/RedisLabs/RedisModulesSDK. There is an example folder. Replace module.c with the above module code. Copy the following files from the decNumber C library to the example folder:

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

Modify the Makefile inside the example folder so that the line beginning with module.so looks like this:

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

Enter this commands in the base directory:

make clean
make

You can test it then with:

redis-server --loadmodule ./module.so

Is that what you are looking for?

Baptista answered 13/5, 2019 at 22:57 Comment(0)
I
1

Maybe getting inspired by event sourcing pattern is something that can solve your problem. Also another way to achieve atomicity is to limit the writing role to only 1 processor whose commands will always be time ordered. (just like redis with lua)

1) You send to redis "events" of balance change stored in a sorted set (for time ordering, timestamp being the score). Only store the "command" you want to do (not the result of the computation). For instance "-1.545466", "+2.07896" etc...

2) Then you consume these events via a Lua script from a single processor (you must be sure that there is only one compute item that accesses this data or you will be in trouble) which can be called with a loop that calls the script every n seconds (you can define your real time quality) ala Apache Storm for instance (a "spout"). The script should return the events from the oldest timestamp up to the latest timestamp, timestamps (scores) should be returned as well (without them you will loose "index") and of course the actual balance.

You should get values that look like:

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

3) In your middleware server (unique, the only one that is allowed to modify the balance), you compute the changes to the balance (using any lib / tech you want in your server, should be lightning fast). You just iterate through the events to compute the balance each time. Note that you will do less mutations in redis thanks to that.

You should get the result

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

4) Once you have the new balance value computed in your server, it is time to send the result and the events you used to redis via a Lua script for:

  • updating balance value, as only one process is allowed to modify it, you should not get any transaction problem, it should also always be time-ordered correctly
  • trimming the sorted set of computed events (oldest and latest timestamps used on step 2 will be used for that) so that these events won't be processed again on next lua call

5) Profit.

Note that operation 4 should be finished before another operation 2 is called, you can set an old semaphore like item in redis to prevent that ("busy" key that prevents operation 2 to run if operation 4 is not finished, you set it when step 2 is launched, you clean it when step 4 is finished, you can also set an eviction on it so if something goes wrong, the eviction will work as a timeout for another iteration to start).

Ingoing answered 13/5, 2019 at 21:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.