说在前面
场景
-
在使用
redis
的有序集合(sorted set
)实现排行榜功能的时候,通常会对成员(member
)的分数(score
)进行一定的设计;例如最简单的分数榜,可以使用:
u i n t 64 ( s c o r e ) < < 32 ∣ u i n t 64 ( 0 × F F F F F F F F − u i n t 32 ( c u r _ t i m e _ s t a m p ) ) uint64(score) << 32 | uint64(0\times FFFFFFFF-uint32(cur\_time\_stamp)) uint64(score)<<32∣uint64(0×FFFFFFFF−uint32(cur_time_stamp))
作为成员的分数(即前32位使用实际的分数,后32位使用最大uint32值减去当前的unix时间戳),这样,在相同分数下,先达成的成员将排在前面。 -
这样的设计理论上其实是没有问题的;但是有序集合中的
score
的数据类型其实是double
,详见 -
当
score
超过一定大小后,就会转为指数形式;例如127.0.0.1:6379> zadd test_key 9007199254740991 a 127.0.0.1:6379> zrange test_key 0 -1 WITHSCORES 1) "a" 2) "9007199254740991" // 这个时候还是很正常的 127.0.0.1:6379> zadd test_key 9007199254740993 a 127.0.0.1:6379> zrange test_key 0 -1 WITHSCORES 1) "a" 2) "9007199254740992" 127.0.0.1:6379> zadd test_key 10007199254740993 a 127.0.0.1:6379> zrange test_key 0 -1 WITHSCORES 1) "a" 2) "10007199254740992" // 只是+1,已经出现精度丢失了 127.0.0.1:6379> zadd test_key 110007199254740993 a 127.0.0.1:6379> zrange test_key 0 -1 WITHSCORES 1) "a" 2) "1.1000719925474099e+17" // 使用指数形式表示了
-
在转成指数形式时候,如果还是将从
redis
中取出来的数据转成uint64
,那么就会转换不过去。 -
这个问题在
score
相对比较小的时候,不太会出现;但是如果score
设计的时候更加细分,比如前32位使用16位的等级+16位的经验,那么就会很容易出现。
解决方式
-
在
score
设计上避免出现问题- 例如将后面的时间戳数据换一种形式记录,比如分钟、小时、天,而不使用秒,从而降低bit位的占用,将更多的bit位放在score上
-
其他
- 暂时没有想到什么更好的方法;这本质上是
uint64
转float64
的问题,当数值大了之后,一定会有精度差异; - 即使使用类型安全的redis库,例如go-redis,也会面临这个问题。
- 暂时没有想到什么更好的方法;这本质上是