Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Redis support arbitrary precision in LUA Scripts

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:

  • it's atomic
  • the simpler INCRBYFLOAT does the subtraction no matter the result and doesn't seems have the proper precision
  • Used the LUA library http://oss.digirati.com.br/luabignum/

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

like image 620
Quadear Avatar asked Apr 26 '19 17:04

Quadear


People also ask

Does Redis use Lua?

Redis lets users upload and execute Lua scripts on the server. Scripts can employ programmatic control structures and use most of the commands while executing to access the database. Because scripts execute in the server, reading and writing data from scripts is very efficient.

What is Redis call?

With redis.call you can execute any Redis command. The first argument is the name of this command followed by its parameters. In the case of the set command, these arguments are key and value. All Redis commands are supported. According to the documentation: Redis uses the same Lua interpreter to run all the commands.

Is there a speed Guide to Redis Lua scripting?

This article is based on A Speed Guide to Redis Lua Scripting from the IBM Compose blog. In this post, we’ll introduce Lua scripting for Redis, but unlike the original article, we’ve made it so all the commands work with IBM Cloud Databases for Redis.

How to debug a Redis list in Lua?

The redis.debug () command is a powerful debugging facility that can be called inside the Redis Lua script in order to log things into the debug console: lua debugger> list -> 1 local a = {1,2,3} 2 local b = false 3 redis.debug (a,b) lua debugger> continue <debug> line 3: {1; 2; 3}, false

What is Redis Redis?

Redis lets users upload and execute Lua scripts on the server. Scripts can employ programmatic control structures and use most of the commands while executing to access the database.

How are scripts executed in Redis?

Scripts are executed in Redis by an embedded execution engine. Presently, Redis supports a single scripting engine, the Lua 5.1 interpreter. Please refer to the Redis Lua API Reference page for complete documentation.


2 Answers

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:

  • documentation: http://speleotrove.com/decimal/decnumber.pdf
  • download: http://download.icu-project.org/files/decNumber/decNumber-icu-368.zip

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?

like image 169
Stephan Schlecht Avatar answered Oct 08 '22 16:10

Stephan Schlecht


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).

like image 24
zenbeni Avatar answered Oct 08 '22 18:10

zenbeni