如何在Redis中创建一个带有“field1 desc,field2 asc”顺序的排序集?

时间:2012-05-11 20:16:57

标签: redis sortedset

我正在尝试在Redis中构建排行榜,并且能够获得最高X分数并检索用户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 个答案:

答案 0 :(得分: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, )))

答案 1 :(得分:0)

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

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

这是ruby中的一个例子:

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

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

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

答案 2 :(得分:0)

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

一些想法:

  • 你可以对时间戳做一些假设,使它们变小。例如,您可以存储“自2012年5月13日以来的分钟数”(例如),而不是存储Unix时间戳。作为交换七位有效数字,这将允许您存储next 19 years
  • 的时间
  • 同样,您可以减少分数中的有效位数。例如,如果您希望分数在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()

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

最后,如果您期望获得真正巨大的分数范围(例如,您预计会有大量分数低于10,000且同样大量的分数超过1,000,000),那么您可以使用两个有序集合: scores-below-100000scores-above-100000

答案 3 :(得分:0)

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

您可以使用全局计数器,而不是在分数中使用时间戳。例如:

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)

要按降序对它们进行排序,请选择一个较高的分数(1<<24,比如说),将其用作global-score-counter的初始值,然后使用DECR代替{{ 1}}。

(如果您使用时间戳,这也适用)

或者,如果你真的非常担心玩家的数量,你可以使用每分数计数器:

INCR