I am new to Redis, and now I need to update the a sorted set if the key exists in another sorted set.
I think it may be clearer to explain by an example, lets say that there are two sorted sets like the following:
set_1
{key_1:val_1, key_2:val_2, key_3:val_3}
set_2
{key_1:val_new_1, key_3:val_new_3, key_4:val_new_4}
Now I am trying to update the first set if the key exists in the second set, so the result should be:
set_1
{key_1:val_new_1, key_2:val_2, key_3:val_new_3}
I have been reading the Redis documentation for a while, and it seems using he SET command with XX option may help:
The SET command supports a set of options that modify its behavior: XX -- Only set the key if it already exist.
But is it possible to avoid running this on each entry in the first set? Maybe using something like zunionstore?
The SET
command only works for regular keys, not for sorted sets.
In sorted sets, you have score-member pairs, so the key-value pair nomenclature of your example is a little confusing. I'll assume key_1, key_2, key_3, ...
are members and val_1, val_2, ...
are scores.
Let's create the sorted sets as follows to review the solution:
> ZADD set_1 1 key_1 2 key_2 3 key_3
(integer) 3
> ZADD set_2 1001 key_1 1003 key_3 1004 key_4
(integer) 3
The default AGGREGATE
is SUM
, it's what we will use all across.
We'll create two sorted sets with the intersection of both, one with the scores of set_1
and one with the scores of set_2
.
> ZINTERSTORE intersect_set_1 2 set_1 set_2 WEIGHTS 1 0
(integer) 2
> ZINTERSTORE intersect_set_2 2 set_1 set_2 WEIGHTS 0 1
(integer) 2
Now, we create a middle-step set for set_1
, where we set the score to zero for those that are in set_2
as well:
> ZUNIONSTORE pre_set_1 2 set_1 intersect_set_1 WEIGHTS 1 -1
(integer) 3
Now we are ready to update set_1, doing a union of:
pre_set_1
: all set_1
but with those also in set_2
set to zero score. intersect_set_2
: the intersection of set_1
and set_2
, with the scores of set_2
.Here is the final command:
> ZUNIONSTORE set_1 2 pre_set_1 intersect_set_2
(integer) 3
Let's see the result:
> ZRANGE set_1 0 -1 WITHSCORES
1) "key_2"
2) "2"
3) "key_1"
4) "1001"
5) "key_3"
6) "1003"
Don't forget to clean up:
> UNLINK pre_set_1 intersect_set_1 intersect_set_2
This solution is not optimal as it uses multiple middle-steps, there is a risk for members added to the original sets in between and it uses more memory than necessary.
The optimal solution would be a Lua script:
local set2 = redis.call('ZRANGE', KEYS[1], '0', '-1', 'WITHSCORES')
local set2length = table.getn(set2)
for i=1,set2length,2 do redis.call('ZADD', KEYS[2], 'XX', set2[i+1], set2[i]) end
return set2length/2
This loops through set_2
, updating set_1
. Note the use of XX
in the ZADD
command, to only update if it exists.
Use as:
EVAL "local set2 = redis.call('ZRANGE', KEYS[1], '0', '-1', 'WITHSCORES') \n local set2length = table.getn(set2) \n for i=1,set2length,2 do print(1) redis.call('ZADD', KEYS[2], 'XX', set2[i+1], set2[i]) end \n return set2length/2" 2 set_2 set_1
The Lua script is atomic due to the single-threaded nature of Redis.