【发布时间】:2019-02-08 15:45:15
【问题描述】:
如果我有 5 个成员,分数如下
a - 1
b - 2
c - 3
d - 3
e - 5
c 的 ZRANK 返回 2,d 的 ZRANK 返回 3
有没有办法以相同的分数获得相同的排名?
示例:ZRANK c = 2, d = 2, e = 3
如果是,那么如何在 spring-data-redis 中实现呢?
【问题讨论】:
如果我有 5 个成员,分数如下
a - 1
b - 2
c - 3
d - 3
e - 5
c 的 ZRANK 返回 2,d 的 ZRANK 返回 3
有没有办法以相同的分数获得相同的排名?
示例:ZRANK c = 2, d = 2, e = 3
如果是,那么如何在 spring-data-redis 中实现呢?
【问题讨论】:
任何真正的解决方案都需要满足原始问题中缺少的要求。我的第一个答案假设一个小数据集,但这种方法至少在 O(N) 中完成密集排名(例如通过 Lua)时无法扩展。
所以,假设有很多有分数的用户,那么for_stack建议的方向比较好,多个数据结构组合在一起。我相信这是他最后一句话的要点。
要存储用户的分数,您可以使用哈希。虽然从概念上讲,您可以使用单个键来存储所有用户分数的哈希值,但实际上您希望对哈希值进行哈希处理,以便它可以扩展。为了简单起见,我将忽略哈希缩放。
这是您在 Lua 中添加(更新)用户分数的方式:
local hscores_key = KEYS[1]
local user = ARGV[1]
local increment = ARGV[2]
local new_score = redis.call('HINCRBY', hscores_key, user, increment)
接下来,我们要跟踪每个离散分值的当前用户数,因此我们为此保留另一个哈希:
local old_score = new_score - increment
local hcounts_key = KEYS[2]
local old_count = redis.call('HINCRBY', hcounts_key, old_score, -1)
local new_count = redis.call('HINCRBY', hcounts_key, new_score, 1)
现在,我们需要维护的最后一件事是每个分数排名,并带有一个排序集。每个新分数都会作为成员添加到 zset 中,而没有更多用户的分数会被删除:
local zdranks_key = KEYS[3]
if new_count == 1 then
redis.call('ZADD', zdranks_key, new_score, new_score)
end
if old_count == 0 then
redis.call('ZREM', zdranks_key, old_score)
end
由于使用了 Sorted Set,这个 3-piece-script 的复杂度为 O(logN),但请注意 N 是离散分数值的数量,而不是系统中的用户。获取用户的密集排名是通过另一个更短更简单的脚本完成的:
local hscores_key = KEYS[1]
local zdranks_key = KEYS[2]
local user = ARGV[1]
local score = redis.call('HGET', hscores_key, user)
return redis.call('ZRANK', zdranks_key, score)
【讨论】:
as dense ranking is done (e.g. via Lua) in O(N) at least 为什么会这样?我可以想到一种使用 Lua(使用 ZSCORE 和 ZCOUNT)的方法,其中密集排名在 O(log(N)) 中完成。
您可以使用两个排序集来实现目标:一个用于成员到得分的映射,另一个用于得分到排名的映射。
添加
ZADD mem_2_score 1 a 2 b 3 c 3 d 5 e
ZADD score_2_rank 1 1 2 2 3 3 5 5
搜索
ZSCORE mem_2_score c,这应该返回分数,即3。ZRANK score_2_rank 3,这应该返回密集排名,即2。为了以原子方式运行它,请将 Add 和 Search 操作包装到 2 个 Lua 脚本中。
【讨论】:
然后是这个拉取请求 - https://github.com/antirez/redis/pull/2011 - 它已经死了,但似乎在运行中进行密集排名。最初的问题/功能请求 (https://github.com/antirez/redis/issues/943) 引起了一些兴趣,所以也许值得恢复它 /cc @antirez :)
【讨论】:
rank在有序集合中是唯一的,得分相同的元素按词法排序(排序)。
没有 Redis 命令可以执行这种“密集排名”
但是,您可以使用 Lua 脚本从排序集中获取范围,并将其缩减为您请求的形式。这可能适用于小型数据集,但您必须设计更复杂的东西才能进行扩展。
【讨论】:
unsigned long zslGetRank(zskiplist *zsl, double score, sds ele) {
zskiplistNode *x;
unsigned long rank = 0;
int i;
x = zsl->header;
for (i = zsl->level-1; i >= 0; i--) {
while (x->level[i].forward &&
(x->level[i].forward->score < score ||
(x->level[i].forward->score == score &&
sdscmp(x->level[i].forward->ele,ele) <= 0))) {
rank += x->level[i].span;
x = x->level[i].forward;
}
/* x might be equal to zsl->header, so test if obj is non-NULL */
if (x->ele && x->score == score && sdscmp(x->ele,ele) == 0) {
return rank;
}
}
return 0;
}
https://github.com/redis/redis/blob/b375f5919ea7458ecf453cbe58f05a6085a954f0/src/t_zset.c#L475
这是 redis 用于计算排序集中排名的代码。现在,它只是根据 Skiplist 中的位置(根据分数排序)给出排名。
What does the skiplistnode variable "span" mean in redis.h?(什么是跨度?)
【讨论】: