I need to be able to make a transaction in redis that does the following:
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 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
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.
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.
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.
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
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.
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.
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:
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(¤tNum, currentValueString, &set);
decNumberFromString(&decrementNum, decrementValueString, &set);
decNumber resultNum;
decNumberSubtract(&resultNum, ¤tNum, &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:
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?
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:
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).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With