Search code examples
ruby-on-rails-3redisleaderboard

Redis zrevrangebyscore, sorting other than lexicographical order


I have implemented a leader board using sorted sets in redis. I want users with same scores to be ordered in chronological order, i.e., user who came first should be ranked higher. Currently redis supports lexicographical order. Is there a way to override that. Mobile numbers are being used as members in sorted set.

One solution that I thought of is appending timestamp in front of mobile numbers and maintaining a hash to map mobile number and timestamp.

$redis.hset('mobile_time', '1234567890', "#{Time.now.strftime('%y%m%d%H%M%S')}")
pref = $redis.hget('mobile_time, '1234567890'')
$redis.zadd('myleaderboard', "1234567890:#{pref}")

That way I can get rank for a given user at any instance by adding a prefix from hash.

Now this is not exactly what I want. This will return opposite of what I want. User who comes early will be placed below user who comes later(both with same score).

Key for user1 = 201210121953**23**01234567890    score: 400
key for user2 = 201210121253**26**09313123523    score: 400 (3 seconds later)

if I use zrevrangebyscore, user2 will be placed higher than user1.

However, there's a way to get the desired rank:

users_with_higher_score_count = $redis.zcount("mysset", "(400", "+inf")
users_with_same_score = $redis.zrangebyscore("mysset", "400", "400")

Now I have the list users_with_same_score with correct ordering. Looking at index I can calculate rank of the user.

To get leader board. I can get members in intervals of 50 and order them through ruby code. But it doesn't seems to be a good way.

I want to know if there's a better approach to do it. Or any improvements that can be made in solution I purposed.

Thanks in advance for your help.

P.S. Scores are in multiples of 50


Solution

  • The score in a sorted set supports double precision floating point numbers, so possibly a better solution would be to store the redis score as highscore.timestamp

    e.g. (pseudocode)

    highscore = 100
    timestamp = now()
    redis.zadd('myleaderboard', highscore + '.' + timestamp, playerId)
    

    This would mean that multiple players who achieved the same high score will also be sorted based on the time they achieved that high score as per the following

    For player 1...

    redis.zadd('myleaderboard', '100.1362345366', "Charles")
    

    For player 2...

    redis.zadd('myleaderboard', '100.1362345399', "Babbage")
    

    See this question for more detail: Unique scoring for redis leaderboard