Reputation: 127
I have an express webserver that performs some operations, in Redis, which need to be atomic, on a key whose value is a list. More specifically, this is more or less the structure of my code
redisclient.lrange(key, 0, -1 (error, items) => {
.
.
.
//some slightly complex code that updates the list of strings
//obtain newItems from items
.
.
.
redisclient.del(key);
//push the newly updated list
for (let item of newItems){
redisclient.rpush(key,item);
}
});
The problem here is that, from my understanding, in order to make these operations atomic I would need to use a Lua script. I don't know anything about Lua though, and converting my JS logic in a Lua script wouldn't be trivial.
Node is single-threaded, but isn't there any alternative to avoid race conditions between different clients in a code like this?
Upvotes: 2
Views: 1606
Reputation: 6754
You can use a transaction so that the DEL and RPUSH commands are executed atomically. See CLIENT.MULTI([COMMANDS]).
You can add a WATCH for your key if you want the transaction not to execute if your list was modified while you processed it. See OPTIMISTIC LOCKS. But here, you need recovery/retry logic in case it fails.
To use WATCH
, you first start watching, then read the list with LRANGE
, do your manipulation, then do MULTI
, DEL
, RPUSH
, EXEC
. The EXEC will fail if the list was modified between WATCH
and EXEC
.
client.watch(key, function( err ){
if(err) throw err;
client.lrange(key, 0, -1, function(err, result) {
if(err) throw err;
// Process the result
client.multi()
.del(key)
.rpush(key, newItems)
.exec(function(err, results) {
/**
* If err is null, it means Redis successfully attempted
* the operation.
*/
if(err) throw err;
/**
* If results === null, it means that a concurrent client
* changed the key while we were processing it and thus
* the execution of the MULTI command was not performed.
*
* NOTICE: Failing an execution of MULTI is not considered
* an error. So you will have err === null and results === null
*/
});
});
});
RPUSH
in Redis supports multiple elements at once. Consider pushing multiple elements at once by using the [send_command][3]
directly, ES6 spread syntax, or passing the array directly (depends on what version are you running).
That said, consider using Lua scripts. Here a little push to get you started:
EVAL "local list1 = redis.call('LRANGE', KEYS[1], 0, -1) for ix,elm in ipairs(list1) do list1[ix] = string.gsub(elm, 'node', 'nodejs') end redis.call('DEL', KEYS[1]) redis.call('RPUSH', KEYS[1], unpack(list1)) return list1" 1 myList
Here a friendly view of the Lua script:
local list1 = redis.call('LRANGE', KEYS[1], 0, -1)
for ix,elm in ipairs(list1) do
list1[ix] = string.gsub(elm, 'node', 'nodejs')
end
redis.call('DEL', KEYS[1])
redis.call('RPUSH', KEYS[1], unpack(list1))
return list1
This simply replaces node
by nodejs
in all elements of the list. See more on string manipulation at the String Library Tutorial.
Upvotes: 3