项⽬⾥⾯有⼤量的榜单需求,很多场景下都是⽤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])