redis源码浅析--十九.排序的实现

环境说明:redis源码版本 5.0.3;我在阅读源码过程做了注释,git地址:https://gitee.com/xiaoangg/redis_annotation
如有错误欢迎指正
参考书籍:《redis的设计与实现》
源码注释:https://gitee.com/xiaoangg/redis_annotation/blob/master/src/sort.c

文章推荐:
redis源码阅读-一--sds简单动态字符串
redis源码阅读--二-链表
redis源码阅读--三-redis散列表的实现
redis源码浅析--四-redis跳跃表的实现
redis源码浅析--五-整数集合的实现
redis源码浅析--六-压缩列表
redis源码浅析--七-redisObject对象(下)(内存回收、共享)
redis源码浅析--八-数据库的实现
redis源码浅析--九-RDB持久化
redis源码浅析--十-AOF(append only file)持久化
redis源码浅析--十一.事件(上)文件事件
redis源码浅析--十一.事件(下)时间事件
redis源码浅析--十二.单机数据库的实现-客户端
redis源码浅析--十三.单机数据库的实现-服务端 - 时间事件
redis源码浅析--十三.单机数据库的实现-服务端 - redis服务器的初始化
redis源码浅析--十四.多机数据库的实现(一)--新老版本复制功能的区别与实现原理
redis源码浅析--十四.多机数据库的实现(二)--复制的实现SLAVEOF、PSYNY
redis源码浅析--十五.哨兵sentinel的设计与实现
redis源码浅析--十六.cluster集群的设计与实现
redis源码浅析--十七.发布与订阅的实现
redis源码浅析--十八.事务的实现
redis源码浅析--十九.排序的实现
redis源码浅析--二十.BIT MAP的实现
redis源码浅析--二十一.慢查询日志的实现
redis源码浅析--二十二.监视器的实现

目录

一 SORT key命令的实现

二 ALPHA选项的实现

三 ASC DESC 选项的实现

四 BY选项的实现

五 带有ALPHA选项 & BY选项的实现

六 LIMIT offset count 选项的实现

七 GET选项的实现


SORT命令 返回或保存给定列表、集合、有序集合 key 中经过排序的元素;
语法:

SORT key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC | DESC] [ALPHA] [STORE destination]

选项:

  • [BY] 默认情况下, SORT 直接按键中的值排序; 通过使用 BY 选项,可以让 uid 按其他键的元素来排序。
    举个例子
      现在有个list键uids,里面存储了uid列表   
    >SORT uids
    "1"
    "2"
    "3"

    上面SORT uids命令,直接按 uids 中的值排序;
    假如想 让 uids 键按照 user_level_{uid} 的大小来排序, 可以使用下面命令:

    >SORT uid BY user_level_*

    上面user_level_* 是一个占位符, 它先取出 uids 中的值, 然后再用这个值来查找相应的键。
    比如在对 uids 列表进行排序时, 程序就会先取出 uid 的值 1 、 2 、 3 、 4 , 然后使用 user_level_1 、 user_level_2 、 user_level_3 和 user_level_4 的值作为排序 uid 的权重。

  • [LIMIT offset count]   排序之后返回元素的数量可以通过 LIMIT 修饰符进行限制, 修饰符接受 offset 和 count 两个参数;
                                         offset 指定要跳过的元素数量;
                                         count 指定跳过 offset 个指定的元素之后,要返回多少个对象;
     
  • [GET] 使用 GET 选项, 可以根据排序的结果来取出相应的键值。
     举个例子:
     下面命令先排序 uid , 再取出键 user_name_{uid} 的值:
    >SORT uid GET user_name_*
  • [ALPHA]  因为SORT命令默认排序对象为数字, 当需要对字符串进行排序时, 需要显式地在SORT命令之后添加 ALPHA 修饰符:
  • [STORE destination]  默认情况下SORT操作只是简单地返回排序结果,并不进行任何保存操作;  STORE 选项指定一个 key 参数,可以将排序结果保存到给定的键上。

注意:集群模式下不支持BY、GET 选项
 

一 SORT key命令的实现

SORT命令的代码实现位于sort.c/sortCommand; 命令的执行过程大致可以分为以下几步:

  1. 检查命令的合法,如sort 只支持对SET、LIST、SORTED SET
  2. 解析命令中选项 保存到相应变量中,并检查是否有语法错误,对limit 选项做了相关优化等
  3. 创建一个server.h/redisSortObject 结构的数组,数组的长度是 要排序的对象的长度(也可能是limit 中选项的长度有根据limit选项进行优化);
    然后将排序对象中的元素 拷贝到该数组中;  
  4. 遍历步骤3中创建的数组,设置每个每个元素的排序权重分数;(会根据BY 选项、ALPHA选项 ,获取不同的权重算法);
  5. 调用快排序函数qpsort或者qsort函数对数组进行排序;
  6. 输出排序后结果到客户端,并判断是否有后续的执行操作 如(GET 、STORE选项);

server.h/redisSortObject的定义;

typedef struct _redisSortObject {
    robj *obj; //被排序的键的值
    union {
        double score; // 排序数字值时使用
        robj *cmpobj; // 排序带有BY选项 ALPHA选项时 使用

    } u; //排序权重
} redisSortObject;

 

二 ALPHA选项的实现

ALPHA选项可以对值中包含字符串的键进行排序;

ALPHA的实现 在代码中主要体现在 步骤5(按照排序分数 进行排序);

对比代码入口位于sort.c/sortCompare 函数

/**
 * sortCompare 是由qsort使用。
 * 考虑到带有附加参数的qsort_r不是标准的,而是特定于BSD的
 * 所以我们必须通过全局“服务器”结构传递排序参数
 */ 
/* sortCompare() is used by qsort in sortCommand(). Given that qsort_r with
 * the additional parameter is not standard but a BSD-specific we have to
 * pass sorting parameters via the global 'server' structure */
int sortCompare(const void *s1, const void *s2) {
    const redisSortObject *so1 = s1, *so2 = s2;
    int cmp;

    if (!server.sort_alpha) {
        // 数字排序。 
        /* Numeric sorting. Here it's trivial as we precomputed scores */
        //.......
    } else {
        //字母数字排序
        /* Alphanumeric sorting */
        if (server.sort_bypattern) { // ALPHA选项 & BY 选项
            //.........
        } else {
            /* Compare elements directly. */
            if (server.sort_store) { // 有store选项
                 //............
            } else { //直接比较对象
                cmp = collateStringObjects(so1->obj,so2->obj);
            }
        }
    }
    return server.sort_desc ? -cmp : cmp;
}

可以看到判断逻辑,如果是单纯的ALPHA比较,直接调用了collateStringObjects进行对比;

三 ASC DESC 选项的实现

同样 ASC DESC的实现 在代码中主要体现在 步骤5(按照排序分数 进行排序);

步骤2解析到的DESC 、ASC选项会存储到 服务器状态的sort_desc属性中;

然后 sort.c/sortCompare 函数 会根据server. sort_desc选项判断返回比较结果:

/**
 * sortCompare 是由qsort使用。
 * 考虑到带有附加参数的qsort_r不是标准的,而是特定于BSD的
 * 所以我们必须通过全局“服务器”结构传递排序参数
 */ 
/* sortCompare() is used by qsort in sortCommand(). Given that qsort_r with
 * the additional parameter is not standard but a BSD-specific we have to
 * pass sorting parameters via the global 'server' structure */
int sortCompare(const void *s1, const void *s2) {
      
    //............
    //............
    return server.sort_desc ? -cmp : cmp;
}

 

四 BY选项的实现

通过BY选项 ,SORT命令可以指定某些字符串键、或者某个哈希键的field作为元素的排序权重;

BY选项的实现主要代码位于步骤4;

在设置这个排序元素的权重时,会判断是否有BY选项;

如果设置了BY选项,会通过lookupKeyByPattern函数去查找匹配的对象;并将排序权重设置成lookupKeyByPattern函数返回的对象;

 

五 带有ALPHA选项 & BY选项的实现

BY选项默认了权重键保存的值是数字。

但是 如果权重键 保存的值是字符串的话,那么就需要使用BY选项的同时,配合使用ALPHA选项;

ALPHA & BY选项实现体现在 步骤4和步骤5;

步骤4中设置权重分数时, 将lookupKeyByPattern返回的对象,通过getDecodedObject函数将编码转换成字符串编码,赋值给比较对象的cmpobj属性;

步骤5中使用sortCompare函数进行比较时,判断如果ALPHA和BY选项同时存在,会使用cmpobj属性进行比较:
 

/**
 * sortCompare 是由qsort使用。
 * 考虑到带有附加参数的qsort_r不是标准的,而是特定于BSD的
 * 所以我们必须通过全局“服务器”结构传递排序参数
 */ 
/* sortCompare() is used by qsort in sortCommand(). Given that qsort_r with
 * the additional parameter is not standard but a BSD-specific we have to
 * pass sorting parameters via the global 'server' structure */
int sortCompare(const void *s1, const void *s2) {
    const redisSortObject *so1 = s1, *so2 = s2;
    int cmp;

    if (!server.sort_alpha) {
        // 数字排序。 
        /* Numeric sorting. Here it's trivial as we precomputed scores */
        //.......
    } else {
        //字母数字排序
        /* Alphanumeric sorting */
        if (server.sort_bypattern) { // ALPHA选项 & BY 选项
          if (!so1->u.cmpobj || !so2->u.cmpobj) {
                // 至少有个一个 cmpobj是null
                /* At least one compare object is NULL */
                if (so1->u.cmpobj == so2->u.cmpobj) //两个cmpobj是null都是NULL
                    cmp = 0;
                else if (so1->u.cmpobj == NULL) //so1的 cmpobj是NULL 则so1小于so2
                    cmp = -1;
                else  // //so2的 cmpobj是NULL 则so1大于so2
                    cmp = 1;
            } else {
                // so1的 cmpobj 和 so2 的 cmpobj 都不是null
                /* We have both the objects, compare them. */
                if (server.sort_store) {
                    cmp = compareStringObjects(so1->u.cmpobj,so2->u.cmpobj);
                } else {
                    /* Here we can use strcoll() directly as we are sure that
                     * the objects are decoded string objects. */
                    cmp = strcoll(so1->u.cmpobj->ptr,so2->u.cmpobj->ptr);
                }
            }
        } else {
            /* Compare elements directly. */
            if (server.sort_store) { // 有store选项
                 //............
            } else { //直接比较对象
                cmp = collateStringObjects(so1->obj,so2->obj);
            }
        }
    }
    return server.sort_desc ? -cmp : cmp;
}

 

六 LIMIT offset count 选项的实现

通过LIMIT选项,可以让SORT命令返回一部分已经排序的元素;

limit的实现注意体现在步骤3 和 步骤6 中;

步骤3中 会根据offset 和count参数 算出获取的的起始索引和终止索引的位置,分别存储到start和end变量中;

步骤6中 输出的时候,只遍历start 和end 区间的元素;


/**
 * SORT命令是Redis中最复杂的命令。
 * 警告:这段代码优化了速度和可读性
 */ 
/* The SORT command is the most complex command in Redis. Warning: this code
 * is optimized for speed and a bit less for readability */
void sortCommand(client *c) {
    //..................     
 
    /**
     * [LIMIT offset count] 
     * limit_start记录offset选项; limit_count记录count选项
     * start end存储经过安全性检查后,最终要获取的起始位置和终止位置  (goto:Perform LIMIT start,count sanity checking)
     */ 
    long limit_start = 0, limit_count = -1, start, end; 
   
    int j, dontsort = 0, vectorlen; //dontsort 1不需要排序 , vectorlen :要的sort的长度,大多数情况下是 排序对象的长度;
    
    redisSortObject *vector; /* Resulting vector to sort */ //要排序的对象中的元素,排序号后的列表

    //.............  

    /**
     * LIMIT offset count 健壮性检查
     */ 
    /* Perform LIMIT start,count sanity checking. */
    start = (limit_start < 0) ? 0 : limit_start;
    end = (limit_count < 0) ? vectorlen-1 : start+limit_count-1;
    if (start >= vectorlen) {
        start = vectorlen-1;
        end = vectorlen-2;
    }
    if (end >= vectorlen) end = vectorlen-1;


    //将需要排序对象中的元素,全部加载到vector中
    /* Load the sorting vector with all the objects to sort */
    vector = zmalloc(sizeof(redisSortObject)*vectorlen);

    //.....
 
    /**
     * 将命令输出发送到输出缓冲区
     * 执行指定的GET/DEL/INCR/DECR操作(如果有)。
     */ 
    /* Send command output to the output buffer, performing the specified
     * GET/DEL/INCR/DECR operations if any. */
    outputlen = getop ? getop*(end-start+1) : end-start+1;
    if (int_conversion_error) { //将排序分数转换成整数时出错了
        addReplyError(c,"One or more scores can't be converted into double");
    } else if (storekey == NULL) {
        //没有指定 STORE 选项,发送排序结果到客户端
        /* STORE option not specified, sent the sorting result to client */
        addReplyMultiBulkLen(c,outputlen);
        for (j = start; j <= end; j++) {
            listNode *ln;
            listIter li;

            if (!getop) addReplyBulk(c,vector[j].obj);
            listRewind(operations,&li);
          
        }
    } else { //指定了STORE选项,将结果存储到List结果中
       // ..............     
    }

    /* Cleanup */
    //.......
}

 

七 GET选项的实现

通过使用GET选项,可以让sort命令对键进行排序后,根据排序的元素,以及GET选项指定的模式,查找返回某些键;

GET命令的实现主要步骤2和步骤6中;

在命令解析过程,如果有个GET选项, redis会把GET选项的参数记录到一个server.h/redisSortOperation结构体列表中,并且会记录GET操作的数量;
server.h/redisSortOperation


typedef struct _redisSortOperation {
    int type;   //操作类型 SORT_OP_* 目前支持GET
    robj *pattern; //操作模式
} redisSortOperation;

命令输出的时候,会判断是否有个GET操作,如果有,则会遍历步骤2创建的redisSortOperation列表,执行get操作:
贴出代码中GET的实现部分:

/**
 * SORT命令是Redis中最复杂的命令。
 * 警告:这段代码优化了速度和可读性
 */ 
/* The SORT command is the most complex command in Redis. Warning: this code
 * is optimized for speed and a bit less for readability */
void sortCommand(client *c) {
    list *operations;  //记录 排序完成后 要后续的操作 ,如GET选项
      int getop = 0; /* GET operation counter */ //GET操作的数量;例如:SORT student GET *_name GET *_calss ; 则getop将被记录成2


    //.................

    /**
     * sort 命令有类似sql的语法,解析这种语法
     */ 
    /* The SORT command has an SQL-alike syntax, parse it */
    while(j < c->argc) {
        //......
        
        
        else if (!strcasecmp(c->argv[j]->ptr,"get") && leftargs >= 1) {
            //集群模式 不支持 GET 选项
            if (server.cluster_enabled) {
                addReplyError(c,"GET option of SORT denied in Cluster mode.");
                syntax_error++;
                break;
            }
            
            //GET操作记录到操作列表
            listAddNodeTail(operations,createSortOperation(
                SORT_OP_GET,c->argv[j+1]));
            getop++;
            j++;
        }
    }
    

    //..............

    if{
        //.......
    }else if (storekey == NULL) {
        //没有指定 STORE 选项,发送排序结果到客户端
        /* STORE option not specified, sent the sorting result to client */
        addReplyMultiBulkLen(c,outputlen);
        for (j = start; j <= end; j++) {
            listNode *ln;
            listIter li;

            //命令没有GET操作,直接回复
            if (!getop) addReplyBulk(c,vector[j].obj);
            
            //有GET操作,执行操作列表
            listRewind(operations,&li);
            while((ln = listNext(&li))) {
                redisSortOperation *sop = ln->value;
                robj *val = lookupKeyByPattern(c->db,sop->pattern,
                    vector[j].obj);

                if (sop->type == SORT_OP_GET) {
                    if (!val) {
                        addReply(c,shared.nullbulk);
                    } else {
                        addReplyBulk(c,val);
                        decrRefCount(val);
                    }
                } else { //还支持除了GET的其他操作,返回错误
                    /* Always fails */
                    serverAssertWithInfo(c,sortval,sop->type == SORT_OP_GET);
                }
            }
        }
    } 

     
}

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值