#需求

某应用用户排行规则:
第一排行维度:用户积分;(积分越高,排行越靠前)
第二排行维度:用户是否为会员;(积分相同时,会员排前面)
第三排行维度:用户最近一次登录时间;(前两个维度值相同时,用户最后一次登录时间越近越靠前)
以下是原始数据:

用户  积分  是否会员  最近一次登录时间戳
A    100  	1       1612754184997
B    200  	0       1612754184997
C    200  	1       1612754184997
D    400  	0       1612754184997
E    200  	1       1612754184998

期望的排序结果:

用户  积分  是否会员  最近一次登录时间戳
D    400  	0       1612754184997
E    200  	1       1612754184998
C    200  	1       1612754184997
B    200  	0       1612754184997
A    100  	1       1612754184997

#使用redis的zset来实现三个维度排序的思路

#score 存储格式

Redis的zset的score是用 double 存储的,
一个 64 位浮点数存储时分为3段:

  • 符号位(Sign) : 0代表正,1代表为负;(第一段,占1位)
  • 指数位(Exponent):用于存储科学计数法中的指数数据;(第二段,占11位)
  • 尾数部分(Mantissa):尾数部分;(第三段,占52位)

在这里插入图片描述

double 存储的有效数据是第三段的52位,超出会损失精度,由于标准规定小数点是在有效数字最前面,所以实际可以存储 53 位数字。

#通过“二进制拆分”,将三个维度合并生成score

利用二进制 64 位 long 分段存储各个维度

首先时间戳如果全存储就太长了,可以通过一些计算缩小一些,先忽略毫秒,然后和一个大数计算差值。

long ts = 1609430400000;//2021.1.1 0:0:0
int MAX_SECOND = 1893427200; // 2030.1.1 0:0:0
int sts = (MAX_SECOND - (int)(ts / 1000));// 283996800

这样时间就缩小到了300000000以内。

接下来进行划分,首位标志位不用,剩下63位,然后我划分高33位存分数,1位存标志位,最后29位存时间戳,存储结构是这样的:
在这里插入图片描述
如果要不损失精度,第一段可存储位数 = 53 - 1 - 29 = 23;
第一段最大值 = 2^33 = 8589934591,不损失精度 = 2^23 = 8388607;
第三段最大值 = 2^29 = 536870911;

使用时可以适当缩短第三段时间戳的长度,或者不追求时间戳一定精确的话,第一段分数可以超出不损失精度的长度,也只会损失一点时间戳的精度而已。

具体生成score的代码就不列举了。

#直接拆分十进制数

可以用二进制拆分当然也可以直接拆分十进制数,为了方便,还可以用小数划分维度,比如将积分放在整数位,标志位和时间戳放在小数位。

A    100.11612754184
B    200.01612754184
C    200.11612754184
D    400.01612754184
E    200.11612754184

不过这样不丢精度存储的分数比二进制拆分小。

#总结

数据量不大的情况下直接使用 读数据库 就可以了,比较方便。用 Redis 的时候,如果排序维度多,可以使用拆分二进制或十进制的方法存储,二进制的优点是存储的数比较大,而且可以用位运算,十进制的优点是计算简单,可读性比较好。各个维度的长度还可以做成配置项,这样就可以满足不同的业务需求了。

#参考

redis zset支持多条件排序


Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐