用redis做榜单,分数相同时根据时间先后排序
redis榜单分数相同时按时间排序
·
项目里面有大量的榜单需求,很多场景下都是用zset来实现的。需求里面无一例外的都提到一个要求:分数相同的情况下,先到该分数的排前面。由于zset是分数优先,分数相同的时候用zset的member的字典序排列,并不满足先来后到这种需求。以前的做法基本都是分数拼凑一个时间量的做法:
- 将zset的score值分成两部分:高位存分数,低位存时间差
- 时间差一般是定一个截止时间x,x-now作为时间差
- 用户a在x1时间达到了分数N,用户b在时间x2达到了分数N,x1<x2,那么x-x1>x-x2,所以拼凑后分数值,a的大于b的,所以a排b前面,实现了先来后到。
这个方法存在这么些问题:
- x不好确定。定的太远,那么x-now的值会比较大,时间差部分需要的位数更多,挤压分数的空间。x定的太小,项目延后下线或者复用后忘记修改也是坑。
- 时间精度的问题。精度越高,需要的位数越多;精度低,那么出现相同时间的概率就越高,时间相同,那又变成字典序了。
当前项目里面用了一个更通用的方法:用浮点数来表征score(zset的score本来就是用double),整数部分表示分数,小数部分表征先来后到:
- 整数部分是分数,小数部分表示先来后到。先到达这个分数,小数值越大,这样排名就靠前。
- 用户a到达分数n的时候,获取当前分数为n(整数部分是n),score值最小的member:
- 如果当前没有member分数是n,说明用户是第一个到达n的(那些曾经是n,但是分数变化了的不用管),那么小数部分就赋一个大点的小数,比如0.9,那么用户的score就是n+0.9。
- 如果查询到的member的score值是n+k1(k1是小数部分),那就构造一个比k1更小的小数k2,a的分数就是n+k2,因为k2<k1,所以a在zset里面会排在这个用户的后面,也就是后到排后面。构造的方法有很多,比如k2=1/(0.1+1/k1)
实现这个功能的lua脚本如下:
local call=redis.call
-- return new score
local function add(key, field, score)
-- get old score
local o=call('zscore', key, field)
local n
if o then
o=math.floor(o)
n=o+score
else
n=score
end
local ss = call('zrangebyscore', key, '('..n, '('..(n+1), 'withscores', 'limit', 0, 1)
local ns
if #ss == 0 then
ns = n+0.9
else
local dec=tonumber(ss[2])-n
ns = n+1/(0.1+1/dec)
end
call('zadd', key, ns, field)
return ns
end
return add(KEYS[1], ARGV[1], ARGV[2])
压测的结果,耗时比直接zadd多了54%,大概是3w/s
更多推荐
已为社区贡献1条内容
所有评论(0)