Redis之zset高级玩法

前言

最近博主收到了一个排序需求,需要我们按照用户权重、状态、性别和在线离线状态对用户进行排序。按照我们传统的数据库里也不行,因为如果要排序我需要取出所有的数据进行排序。假如说我这时候有很大的数据量该怎么办?我不可能全部取出来然后通过java排序,这样的性能是非常低的。那么有小伙伴肯定就会说,用zset直接排序。那么zset我们都知道是通过score排序的,但是我们怎么办把多权重排序放到zset里呢?

核心技术

首先按照我的用户权重、状态、性别和在线状态这4个维度的角度我们先按照需求分一下他们的排序优先级。首先状态肯定不用说 优先级最高的,其次就是性别,然后是在房间在线离线状态、最后是用户权重。
状态>性别>在线离线在房间状态>用户权重。
那么我们就有两种做法。1:十进制拆分法。2:二进制拆分法。

十进制拆分

这个非常好理解。就是通过十进制的整数把我们的条件拆分成不通的位数,最后优先级越高的位数越大。比如我的用户权重最小,但是他有10个权重。那么我就可以将我们的10放在十分位上。这样取出来的时候就是按照权重排好序的。
例如
在这里插入图片描述
我们第一个条件已经好了。这里提个醒,我们再做排序之前一定要先思考好条件优先级最后自己对优先级做个排序,我们按照优先级最小的先做,这样最后算出来的score才是正确的排好序的。
那么第二个条件是用户在线状态。那么在线状态只有三个,由于我的十分位已经被占用了,那么我就用百分位来代替。100是离线,200是在线,300是在房间。最后我们的分数也变成了如下这样:
在这里插入图片描述
以此类推,其他的条件值也可以这么玩。那么当我们按照条件优先级算好分数存储进zset里就已经是帮我们排好序的一个集合了,我们只需要拿来用既可。第一种非常简单,也非常好理解。那么接下来我要说一下第二种进阶版。

二进制拆分法

首先,我们先了解一下zset的跳表结构:

typedef struct zskiplistNode {
    
    
    
        // 成员对象
        robj *obj;
    
        // 分值
        double score;
    
        // 后退指针
        struct zskiplistNode *backward;
    
        // 层
        struct zskiplistLevel {
    
    
            // 前进指针
            struct zskiplistNode *forward;
    
            // 跨度
            unsigned int span;
        } level[];
    
    } zskiplistNode;

我们可以看到,他的score实际上是一个double类型的一个数据。那么我们先了解一下它的double。
首先他的double类型是一个IEEE 754 标准的 double 浮点数。由此我们可以知道它的结构
在这里插入图片描述

S:符号位 1为负数,0为正数
Exp: 指数字,第二段,占11位,移码方式表示正负数,排除全0和全1表示零和无穷大的特殊情况,指数部分是−1022~+1023加上1023,指数值的大小从1~2046
Fraction:有效数字,占52位,定点数,小数点放在尾数最前面
知道了这个以后,我们根据其条件不同,可以用不同的位数去存取数据。那么由于博主的条件比较少,就用了int类型来存储我们的数据。
首先我需要先构思每个条件需要多少位

  1. 用户权重:由于用户权重只有10个。那么我将10换算成二进制的时候,它就是1010,也就是占了4位,所以我们将用户权重存储在int类型数值的低4位如下图所示:
    在这里插入图片描述
    这时候我们虽然用二进制表示,但是在redis里面他会给你显示十进制数值,也就是10。那么再回归到我们刚刚讲的十进制拆分,实际上,二进制拆分也跟十进制差不多,只不过我们是将10进制改变成为二进制而已,但是大体思路还是10进制的思路。
  2. 在线状态:我们的在线状态条件有三个:1.在房间、2.在线、3.离线。那么由此我们可以得知我们只需要2位就可以算出用户的在线状态,那么我们可以把0当成离线,1当作在线,2当作在房间。换算成二进制就是离线:00 、在线:01、在房间:10。由于我们之前还有用户权重,所以在线状态我们需要存储到第四位和第五位,此时我们的bit就变成了如下所示:我以在线状态为在房间状态所示:

在这里插入图片描述
我们将它换算成十进制就是:42.那么存储在redis里的分数也会是42。

  1. 性别:由于性别是一个很中间性的优先级,那么我可以存储在整数bit的中间位,此时我们就可以写成:
    在这里插入图片描述

那么我们再取数据的时候,假如这时候我要取出全部男性数据,那么我就可以将我的分数范围设置在
0 ~ 1 << 16 的范围之间,那么这个范围的数据就是男性数据,而高于 1<< 16的就是女性数据。
那么位运算的符号是什么意思,一会我会详细跟大家解释。但是左移符号我觉得应该都能看懂~

  1. 状态:在我的认知里,应该没有什么比状态优先级更大的了,因为我们状态通常都是开启、关闭等等。那么由于我这里只有两种状态,同样我只需要用0和1来表示不同的状态就可以。比如我用1表示关闭状态,0表示开启状态,那么我就可以写成这样:

在这里插入图片描述

那么我在取数据的时候,我只需要将score区间设置成0~1<<30范围内的数据,就可以将全部状态为开启的用户取出。那么知道了这些后,我们如何计算分数呢?

二进制拆分计算score

首先我们先定义一下一个全局的静态常量:

/**
     * 用户在线状态bit长度
     */
    public static final int onlineStatusBits = 2;

    /**
     * 用户权重bit长度
     */
    public static final int WEIGHT_BITS= 4;

    /**
     * 用于保存用户性别 1女 0男
     */
    public static final int SCORE_WOMAN = 1 << 16;

    /**
     * 用户状态
     */
    public static final int SCORE_STATUS = 1 << 30;

那么再计算分数的时候,以我现在的状态是在房间状态,并且我的用户权重是5为例,我们可以写成如下方式:

int score = (3 << WEIGHT_BITS) | 5;

将三向左移动weightBits的目的是为了让出低位的空间,让我们的用户权重有地方放。那么再通过按位或就可以得出score。| 这个符号的含义是:两个数值进行二进制比较的时候,两个数值的位数都是0才为0,如果有一个为1,就是1.
我以上面的代码为例子:
我们3 << weightBits 换算成二进制:
0011 0000
5的二进制:
0000 0101
那么此时我们通过按位或计算出来的结果就是:
0011 0101
我们发现最终的这个结果高4位是我们的在线状态的值,第四位就是我们的权重。
如果我们要取出来数据怎么办呢?

如何从score中取出对应的值

其实这个也很简单,我们以刚刚的结果 0011 0101来距离。首先我们先说一下我们如何取出位数上的数据呢?

(1 << bit) - 1 & score

就可以,首先我们说一下& 按位与 这个符号的含义。那么他是这样的,两个二进制数值进行对比,如果都为1,我才为1,否则就是0.
那么通过这个,我们要计算用户权重就很简单,我们只需要这样写既可:

int weight = score & ((1 << WEIGHT_BITS) - 1);

首先我们的1左移4位之后它的二进制变成如下表示:
0001 0000
此时我们肯定是不能进行&运算的这样肯定会出错,那么只需要-1,就可以变成
0000 1111
此时我们再计算分数
0011 0101
通过按位与获得的结果
0000 0101
我们也就取出了用户的权重值。

而我们想要去其他高位的数值怎么办?我们还是以刚刚的例子距离,我们只需要这样写

int onlineStatus = (score >> WEIGHT_BITS) & ((1 << ONLINE_STATUS_BITS) - 1);

将分数右移到权重上,例如 0011 0101 >> 4位
就会变成
0000 0011
这时候在通过按位与运算
0000 0011
结果就是我们的权重值的分数了,其他的算法也同理。

好啦,今天的文章就到这里了。喜欢的话记得评论点赞转发收藏一下哦~

猜你喜欢

转载自blog.csdn.net/qq_39685066/article/details/109379747