2

我正在尝试在 Redis 中建立排行榜,并能够获得最高X分并检索 user 的排名Y

Redis 中的排序列表看起来很容易,除了一个问题 - 我需要的分数不仅按实际分数排序,还按日期排序(所以早先获得相同分数的人将排在首位)。SQL 查询将是:

select * from scores order by score desc, date asc

在 Redis 中的排序集上运行zrevrange使用如下内容:

select * from scores order by score desc, key desc

这将使用户在字典上具有更大的键。

我能想到的一种解决方案是对排序集中的分数字段进行一些操作,以生成由分数和时间戳组成的组合数字。

例如,对于555带有时间戳111222333的分数,最终分数可能类似于555.111222333将新分数置于旧分数之上(不完全是我需要的,但可以进一步调整)。

这会起作用,但只适用于小数字,因为排序集中的分数只有 16 位有效数字,因此其中 10 位将立即浪费在时间戳上,没有太多空间留给实际分数。

任何想法如何使排序集以正确的顺序排列值?我真的希望最终结果是一个排序集(以便轻松检索用户的排名),即使它需要一些临时结构和排序来构建这样的集合。

4

4 回答 4

1

实际上,我之前的所有答案都很糟糕。忽略我以前的所有答案(尽管为了其他人的利益,我将把它们留在身边)。

这就是你实际应该这样做的方式:

  • 仅存储 zset 中的分数
  • 单独存储玩家每次达到该分数的列表。

例如:

score_key = <whatever unique key you want to use for this score>
redis('ZADD scores-sorted %s %s' %(score, score))
redis('RPUSH score-%s %s' %(score, score_key))

然后读取分数:

top_score_keys = []
for score in redis('ZRANGE scores-sorted 0 10'):
    for score_key in redis('LRANGE score-%s 0 -1' %(score, )):
        top_score_keys.append(score_key)

显然你想在那里做一些优化(例如,只阅读score-列表的大块,而不是阅读整个内容)。

但这绝对是做到这一点的方法。

用户排名将是直截了当的:对于每个用户,跟踪他们的高分:

redis('SET highscores-%s %s' %(user_id, user_high_score))

然后使用以下方法确定他们的排名:

user_high_score = redis('GET highscores-%s' %(user_id, ))
score_rank = int(redis('ZSCORE scores-sorted %s' %(user_high_score, )))
score_rank += int(redis('LINDEX score-%s' %(user_high_score, )))
于 2012-05-13T21:23:23.930 回答
0

这并不是真正的完美解决方案,但如果您制作一个更接近当前时间的自定义纪元,那么您将需要更少的数字来表示它。

例如,如果您使用 2012 年 1 月 1 日作为您的纪元,那么您(目前)只需要 8 位数字来表示时间戳。

这是一个红宝石的例子:

(Time.new(2012,01,01,0,0,0)-Time.now).to_i

这将使您在时间戳需要 9 位数字之前大约 3 年,此时您可以执行一些维护以再次向前移动自定义纪元。

但是,我很想听听是否有人有更好的主意,因为我有完全相同的问题。

于 2012-05-13T20:48:29.887 回答
0

注意:这个答案几乎可以肯定是次优的;请参阅https://stackoverflow.com/a/10575370/71522

几个想法:

  • 您可以对时间戳做出一些假设以使它们更小。例如,您可以存储“自 2012 年 5 月 13 日以来的分钟数”(例如),而不是存储 Unix 时间戳。作为七位有效数字的交换,您可以存储未来 19 年的时间。
  • 同样,您可以减少分数中有效数字的数量。例如,如果您希望分数在 7 位范围内,则可以在将它们存储在排序列表中时将它们除以 10、100 或 1000,然后使用排序列表的结果来访问实际分数,排序那些在应用程序级别。

例如,使用上述两种方法(在可能有错误的伪代码中):

score_small = int(score / 1000)
time_small = int((time - 1336942269) / 60)
score_key = uuid()
redis('SET full-score-%s "%s %s"' %(score_key, score, time))
redis('ZADD sorted-scores %s.%s %s' %(score_small, time_small, score_key))

然后加载它们(大约):

top_scores = []
for score_key in redis('ZRANGE sorted-scores 0 10'):
    score_str, time_str = redis('GET full-score-%s' %(score_key, )).split(" ")
    top_scores.append((int(score_str), int(time_str))
top_scores.sort()

O(n) GET此操作甚至可以使用EVAL命令完全在 Redis 内部完成(避免操作的网络开销) (尽管我对 Lua 的了解还不够,无法自信地提供示例实现)。

最后,如果您期望分数的范围非常大(例如,您期望会有大量分数低于 10,000,而同样多的分数超过 1,000,000),那么您可以使用两个排序集:scores-below-100000scores-above-100000.

于 2012-05-13T20:49:56.110 回答
0

(Note: this answer is almost certainly suboptimial; see https://stackoverflow.com/a/10575370/71522)

Instead of using a timestamp in the score, you could use a global counter. For example:

score_key = <whatever unique key you want to use for this score>
score_number = redis('INCR global-score-counter')
redis('ZADD sorted-scores %s.%s %s' %(score, score_number, score_key)

And to sort them in descending order, pick a large score count (1<<24, say), use that as the initial value of global-score-counter, then use DECR instead of INCR.

(this would also apply if you are using a timestamp)

Alternately, if you really incredibly worried about the number of players, you could use a per-score counter:

score_key = <whatever unique key you want to use for this score>
score_number = redis('HINCR score-counter %s' %(score, ))
redis('ZADD sorted-scores %s.%s %s' %(score, score_number, score_key))
于 2012-05-13T21:15:22.873 回答