redis源码分析与思考(十四)——列表类型的命令实现(t_list.c)

    列表类型是用来存贮多个字符串对象的结构。一个列表可以存贮232-1个元素,可以对列表两端进行插入(push)、弹出(pop),还可以获取指定范围内的元素列表、获取指定索引的元素等等,它可以灵活的充当栈和队列的角色。下面列出列表的命令:

列表命令

操作说明命令时间复杂度
向尾部添加rpush key value [value…]O(n)
向头部添加lpush key value [value…]O(n)
插入在指定键前/后linsert key before/after pivot valueO(n)
返回范围内的键lrange key start endO(n)
返回索引的键lindex key indexO(n)
返回列表的长度llen key indexO(1)
弹出头部节点lpop keyO(1)
弹出尾部节点rpop keyO(1)
删除指定cout数目且值等于value的键lremkey count valueO(n)
删除指定范围的键ltrim key start endO(n)
修改指定索引的键值lset key index valueO(n)
阻塞操作blpop、brpopO(1)

编码的转换

    上次在介绍redis对象时提到过,列表对象的编码格式会在一定情况下从ziplist转变成adlist链表。列表每次push元素时,都会检测是否满足这一情况:

/*
 * 对输入值 value 进行检查,看是否需要将 subject 从 ziplist 转换为双端链表,
 * 以便保存值 value 。
 * 函数只对 REDIS_ENCODING_RAW 编码的 value 进行检查,
 * 因为整数编码的值不可能超长。
 */
 //server.list_max_ziplist_value是服务器的属性,可更改,默认64字节
void listTypeTryConversion(robj *subject, robj *value) {
    // 确保 subject 为 ZIPLIST 编码
    if (subject->encoding != REDIS_ENCODING_ZIPLIST) return;
    if (sdsEncodedObject(value) &&
        // 看字符串是否过长
        sdslen(value->ptr) > server.list_max_ziplist_value)
            // 将编码转换为双端链表
            listTypeConvert(subject,REDIS_ENCODING_LINKEDLIST);
}

/*
 * 将列表的底层编码从 ziplist 转换成双端链表
 */
void listTypeConvert(robj *subject, int enc) {
//定义一个链表的迭代器
    listTypeIterator *li;
    listTypeEntry entry;
    redisAssertWithInfo(NULL,subject,subject->type == REDIS_LIST);
    // 转换成双端链表
    if (enc == REDIS_ENCODING_LINKEDLIST) {
        list *l = listCreate();
        //设置链表的释放函数
        listSetFreeMethod(l,decrRefCountVoid);
        /* listTypeGet returns a robj with incremented refcount */
        // 遍历 ziplist ,并将里面的值全部添加到双端链表中
        li = listTypeInitIterator(subject,0,REDIS_TAIL);
        while (listTypeNext(li,&entry)) listAddNodeTail(l,listTypeGet(&entry));
        listTypeReleaseIterator(li);
        // 更新编码
        subject->encoding = REDIS_ENCODING_LINKEDLIST;
        // 释放原来的 ziplist
        zfree(subject->ptr);
        // 更新对象值指针
        subject->ptr = l;
    } else {
        redisPanic("Unsupported list conversion");
    }
}

    也就是说,当列表里插入的元素字节长大于64字节时,列表会启动编码转换程序。

列表的新增

    不仅是插入的元素字节长大于默认的64字节会激动编码转换,新增元素的底层实现,在插入之前会检测列表的总长度是否大于默认ziplist最大长度也会引起编码转换,默认长度是512个元素,可以在服务器的配置中更改,列表插入底层实现如下:

//server.list_max_ziplist_entries表示最大长度,where表示插入到表头还是表尾
void listTypePush(robj *subject, robj *value, int where) {
    /* Check if we need to convert the ziplist */
    // 是否需要转换编码?
    listTypeTryConversion(subject,value);
    if (subject->encoding == REDIS_ENCODING_ZIPLIST &&
        ziplistLen(subject->ptr) >= server.list_max_ziplist_entries)
            listTypeConvert(subject,REDIS_ENCODING_LINKEDLIST);
    // ZIPLIST
    if (subject->encoding == REDIS_ENCODING_ZIPLIST) {
        int pos = (where == REDIS_HEAD) ? ZIPLIST_HEAD : ZIPLIST_TAIL;
        // 取出对象的值,因为 ZIPLIST 只能保存字符串或整数
        value = getDecodedObject(value);
        subject->ptr = ziplistPush(subject->ptr,value->ptr,sdslen(value->ptr),pos);
        decrRefCount(value);
    // 双端链表
    } else if (subject->encoding == REDIS_ENCODING_LINKEDLIST) {
        if (where == REDIS_HEAD) {
            listAddNodeHead(subject->ptr,value);
        } else {
            listAddNodeTail(subject->ptr,value);
        }
        incrRefCount(value);
    // 未知编码
    } else {
        redisPanic("Unknown list encoding");
    }
}


/*
 * 将对象 value 插入到列表节点的之前或之后。
 * where 参数决定了插入的位置:
 *  - REDIS_HEAD 插入到节点之前
 *  - REDIS_TAIL 插入到节点之后
 */
void listTypeInsert(listTypeEntry *entry, robj *value, int where) {
    robj *subject = entry->li->subject;
    // 插入到 ZIPLIST
    if (entry->li->encoding == REDIS_ENCODING_ZIPLIST) {
        // 返回对象未编码的值
        value = getDecodedObject(value);
        if (where == REDIS_TAIL) {
            unsigned char *next = ziplistNext(subject->ptr,entry->zi);
            /* When we insert after the current element, but the current element
             * is the tail of the list, we need to do a push. */
            if (next == NULL) {
                // next 是表尾节点,push 新节点到表尾
                subject->ptr = ziplistPush(subject->ptr,value->ptr,sdslen(value->ptr),REDIS_TAIL);
            } else {
                // 插入到到节点之后
                subject->ptr = ziplistInsert(subject->ptr,next,value->ptr,sdslen(value->ptr));
            }
        } else {
            subject->ptr = ziplistInsert(subject->ptr,entry->zi,value->ptr,sdslen(value->ptr));
        }
        decrRefCount(value);
    // 插入到双端链表
    } else if (entry->li->encoding == REDIS_ENCODING_LINKEDLIST) {
        if (where == REDIS_TAIL) {
            listInsertNode(subject->ptr,entry->ln,value,AL_START_TAIL);
        } else {
            listInsertNode(subject->ptr,entry->ln,value,AL_START_HEAD);
        }
        incrRefCount(value);
    } else {
        redisPanic("Unknown list encoding");
    }
}

    PUSH系列命令的底层实现:

void pushGenericCommand(redisClient *c, int where) {
    int j, waiting = 0, pushed = 0;
    // 取出列表对象
    robj *lobj = lookupKeyWrite(c->db,c->argv[1]);
    // 如果列表对象不存在,那么可能有客户端在等待这个键的出现
    int may_have_waiting_clients = (lobj == NULL);
    if (lobj && lobj->type != REDIS_LIST) {
        addReply(c,shared.wrongtypeerr);
        return;
    }
    // 将列表状态设置为就绪
    if (may_have_waiting_clients) signalListAsReady(c,c->argv[1]);
    // 遍历所有输入值,并将它们添加到列表中
    for (j = 2; j < c->argc; j++) {
        // 编码值
        c->argv[j] = tryObjectEncoding(c->argv[j]);
        // 如果列表对象不存在,那么创建一个,并关联到数据库
        if (!lobj) {
            lobj = createZiplistObject();
            dbAdd(c->db,c->argv[1],lobj);
        }
        // 将值推入到列表
        listTypePush(lobj,c->argv[j],where);
        pushed++;
    }
    // 返回添加的节点数量
    addReplyLongLong(c, waiting + (lobj ? listTypeLength(lobj) : 0));
    // 如果至少有一个元素被成功推入,那么执行以下代码
    if (pushed) {
        char *event = (where == REDIS_HEAD) ? "lpush" : "rpush";
        // 发送键修改信号
        signalModifiedKey(c->db,c->argv[1]);
        // 发送事件通知
        notifyKeyspaceEvent(REDIS_NOTIFY_LIST,event,c->argv[1],c->db->id);
    }
    server.dirty += pushed;
}

索引

    索引的策略很简单,判断是哪种编码格式调用哪种遍历:

void lindexCommand(redisClient *c) {
    robj *o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk);
    if (o == NULL || checkType(c,o,REDIS_LIST)) return;
    long index;
    robj *value = NULL;
    // 取出整数值对象 index
    if ((getLongFromObjectOrReply(c, c->argv[2], &index, NULL) != REDIS_OK))
        return;
    // 根据索引,遍历 ziplist ,直到指定位置
    if (o->encoding == REDIS_ENCODING_ZIPLIST) {
        unsigned char *p;
        unsigned char *vstr;
        unsigned int vlen;
        long long vlong;
        p = ziplistIndex(o->ptr,index);
        if (ziplistGet(p,&vstr,&vlen,&vlong)) {
            if (vstr) {
                value = createStringObject((char*)vstr,vlen);
            } else {
                value = createStringObjectFromLongLong(vlong);
            }
            addReplyBulk(c,value);
            decrRefCount(value);
        } else {
            addReply(c,shared.nullbulk);
        }
    // 根据索引,遍历双端链表,直到指定位置
    } else if (o->encoding == REDIS_ENCODING_LINKEDLIST) {
        listNode *ln = listIndex(o->ptr,index);
        if (ln != NULL) {
            value = listNodeValue(ln);
            addReplyBulk(c,value);
        } else {
            addReply(c,shared.nullbulk);
        }
    } else {
        redisPanic("Unknown list encoding");
    }
}

pop操作

    pop操作其实就是将队列的pop操作而已,弹出元素后如果列表为空,则删除该列表:

void popGenericCommand(redisClient *c, int where) {
    // 取出列表对象
    robj *o = lookupKeyWriteOrReply(c,c->argv[1],shared.nullbulk);
    if (o == NULL || checkType(c,o,REDIS_LIST)) return;
    // 弹出列表元素
    robj *value = listTypePop(o,where);
    // 根据弹出元素是否为空,决定后续动作
    if (value == NULL) {
        addReply(c,shared.nullbulk);
    } else {
        char *event = (where == REDIS_HEAD) ? "lpop" : "rpop";
//回复给客户端,弹出的value
        addReplyBulk(c,value);
        //引用计数减1
        decrRefCount(value);
        notifyKeyspaceEvent(REDIS_NOTIFY_LIST,event,c->argv[1],c->db->id);
        if (listTypeLength(o) == 0) {
            notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"del",
                                c->argv[1],c->db->id);
            dbDelete(c->db,c->argv[1]);
        }
        signalModifiedKey(c->db,c->argv[1]);
        server.dirty++;
    }
}

裁剪操作

//LTRIM命令
void ltrimCommand(redisClient *c) {
    robj *o;
    long start, end, llen, j, ltrim, rtrim;
    list *list;
    listNode *ln;
    // 取出索引值 start 和 end
    if ((getLongFromObjectOrReply(c, c->argv[2], &start, NULL) != REDIS_OK) ||
        (getLongFromObjectOrReply(c, c->argv[3], &end, NULL) != REDIS_OK)) return;
    // 取出列表对象
    if ((o = lookupKeyWriteOrReply(c,c->argv[1],shared.ok)) == NULL ||
        checkType(c,o,REDIS_LIST)) return;
    // 列表长度
    llen = listTypeLength(o);
    /* convert negative indexes */
    // 将负数索引转换成正数索引
    if (start < 0) start = llen+start;
    if (end < 0) end = llen+end;
    if (start < 0) start = 0;
    /* Invariant: start >= 0, so this test will be true when end < 0.
     * The range is empty when start > end or start >= length. */
    if (start > end || start >= llen) {
        /* Out of range start or start > end result in empty list */
        ltrim = llen;
        rtrim = 0;
    } else {
        if (end >= llen) end = llen-1;
        ltrim = start;
        rtrim = llen-end-1;
    }
    /* Remove list elements to perform the trim */
    // 删除指定列表两端的元素
    if (o->encoding == REDIS_ENCODING_ZIPLIST) {
        // 删除左端元素
        o->ptr = ziplistDeleteRange(o->ptr,0,ltrim);
        // 删除右端元素
        o->ptr = ziplistDeleteRange(o->ptr,-rtrim,rtrim);
    } else if (o->encoding == REDIS_ENCODING_LINKEDLIST) {
        list = o->ptr;
        // 删除左端元素
        for (j = 0; j < ltrim; j++) {
            ln = listFirst(list);
            listDelNode(list,ln);
        }
        // 删除右端元素
        for (j = 0; j < rtrim; j++) {
            ln = listLast(list);
            listDelNode(list,ln);
        }
    } else {
        redisPanic("Unknown list encoding");
    }
    // 发送通知
    notifyKeyspaceEvent(REDIS_NOTIFY_LIST,"ltrim",c->argv[1],c->db->id);
    // 如果列表已经为空,那么删除它
    if (listTypeLength(o) == 0) {
        dbDelete(c->db,c->argv[1]);
        notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"del",c->argv[1],c->db->id);
    }
    signalModifiedKey(c->db,c->argv[1]);
    server.dirty++;
    addReply(c,shared.ok);
}

移除列表中指定的值

void lremCommand(redisClient *c) {
    robj *subject, *obj;
    // 编码目标对象 elem
    obj = c->argv[3] = tryObjectEncoding(c->argv[3]);
    long toremove;
    long removed = 0;
    listTypeEntry entry;
    // 取出指定删除模式的 count 参数
    if ((getLongFromObjectOrReply(c, c->argv[2], &toremove, NULL) != REDIS_OK))
        return;
    // 取出列表对象
    subject = lookupKeyWriteOrReply(c,c->argv[1],shared.czero);
    if (subject == NULL || checkType(c,subject,REDIS_LIST)) return;
    /* Make sure obj is raw when we're dealing with a ziplist */
    if (subject->encoding == REDIS_ENCODING_ZIPLIST)
        obj = getDecodedObject(obj);
    listTypeIterator *li;
    // 根据 toremove 参数,决定是从表头还是表尾开始进行删除
    if (toremove < 0) {
        toremove = -toremove;
        li = listTypeInitIterator(subject,-1,REDIS_HEAD);
    } else {
        li = listTypeInitIterator(subject,0,REDIS_TAIL);
    }
    // 查找,比对对象,并进行删除
    while (listTypeNext(li,&entry)) {
        if (listTypeEqual(&entry,obj)) {
            listTypeDelete(&entry);
            server.dirty++;
            removed++;
            // 已经满足删除数量,停止
            if (toremove && removed == toremove) break;
        }
    }
    listTypeReleaseIterator(li);
    /* Clean up raw encoded object */
    if (subject->encoding == REDIS_ENCODING_ZIPLIST)
        decrRefCount(obj);
    // 删除空列表
    if (listTypeLength(subject) == 0) dbDelete(c->db,c->argv[1]);
    addReplyLongLong(c,removed);
    if (removed) signalModifiedKey(c->db,c->argv[1]);
}

阻塞版pop

    阻塞版本的pop其实与普通pop操作没两样,只是加了阻塞操作而已,假如列表为空,阻塞时间为3s,那么客户端要等到3s后返回,如果在期间push了元素,那么立即返回该元素。其底层实现如下:

void blockingPopGenericCommand(redisClient *c, int where) {
    robj *o;
    mstime_t timeout;
    int j;
    // 取出 timeout 参数
    if (getTimeoutFromObjectOrReply(c,c->argv[c->argc-1],&timeout,UNIT_SECONDS)
        != REDIS_OK) return;
    // 遍历所有列表键
    for (j = 1; j < c->argc-1; j++) {
        // 取出列表键
        o = lookupKeyWrite(c->db,c->argv[j]);
        // 有非空列表?
        if (o != NULL) {
            if (o->type != REDIS_LIST) {
                addReply(c,shared.wrongtypeerr);
                return;
            } else {
                // 非空列表
                if (listTypeLength(o) != 0) {
                    /* Non empty list, this is like a non normal [LR]POP. */
                    char *event = (where == REDIS_HEAD) ? "lpop" : "rpop";

                    // 弹出值
                    robj *value = listTypePop(o,where);
                    redisAssert(value != NULL);
                    // 回复客户端
                    addReplyMultiBulkLen(c,2);
                    // 回复弹出元素的列表
                    addReplyBulk(c,c->argv[j]);
                    // 回复弹出值
                    addReplyBulk(c,value);
                    decrRefCount(value);
                    notifyKeyspaceEvent(REDIS_NOTIFY_LIST,event,
                                        c->argv[j],c->db->id);
                    // 删除空列表
                    if (listTypeLength(o) == 0) {
                        dbDelete(c->db,c->argv[j]);
                        notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"del",
                                            c->argv[j],c->db->id);
                    }
                    signalModifiedKey(c->db,c->argv[j]);
                    server.dirty++;
                    /* Replicate it as an [LR]POP instead of B[LR]POP. */
                    // 传播一个 [LR]POP 而不是 B[LR]POP
                    rewriteClientCommandVector(c,2,
                        (where == REDIS_HEAD) ? shared.lpop : shared.rpop,
                        c->argv[j]);
                    return;
                }
            }
        }
    }
    /* If we are inside a MULTI/EXEC and the list is empty the only thing
     * we can do is treating it as a timeout (even with timeout 0). */
    // 如果命令在一个事务中执行,那么为了不产生死等待
    // 服务器只能向客户端发送一个空回复
    if (c->flags & REDIS_MULTI) {
        addReply(c,shared.nullmultibulk);
        return;
    }
    /* If the list is empty or the key does not exists we must block */
    // 所有输入列表键都不存在,只能阻塞了,阻塞其指定的时间
    blockForKeys(c, c->argv + 1, c->argc - 2, timeout, NULL);
}
// 根据给定数量的 key ,对给定客户端进行阻塞
// 参数:
// keys    任意多个 key
// numkeys keys 的键数量
// timeout 阻塞的最长时限
// target  在解除阻塞时,将结果保存到这个 key 对象,而不是返回给客户端
//         只用于 BRPOPLPUSH 命令
void blockForKeys(redisClient *c, robj **keys, int numkeys, mstime_t timeout, robj *target) {
    dictEntry *de;
    list *l;
    int j;
    // 设置阻塞状态的超时和目标选项
    c->bpop.timeout = timeout;
    // target 在执行 RPOPLPUSH 命令时使用
    c->bpop.target = target;
    if (target != NULL) incrRefCount(target);
    // 关联阻塞客户端和键的相关信息
    for (j = 0; j < numkeys; j++) {
        /* If the key already exists in the dict ignore it. */
        // c->bpop.keys 是一个集合(值为 NULL 的字典)
        // 它记录所有造成客户端阻塞的键
        // 以下语句在键不存在于集合的时候,将它添加到集合
        if (dictAdd(c->bpop.keys,keys[j],NULL) != DICT_OK) continue;
        incrRefCount(keys[j]);
        /* And in the other "side", to map keys -> clients */
        // c->db->blocking_keys 字典的键为造成客户端阻塞的键
        // 而值则是一个链表,链表中包含了所有被阻塞的客户端
        // 以下程序将阻塞键和被阻塞客户端关联起来
        de = dictFind(c->db->blocking_keys,keys[j]);
        if (de == NULL) {
            // 链表不存在,新创建一个,并将它关联到字典中
            int retval;
            /* For every key we take a list of clients blocked for it */
            l = listCreate();
            retval = dictAdd(c->db->blocking_keys,keys[j],l);
            incrRefCount(keys[j]);
            redisAssertWithInfo(c,keys[j],retval == DICT_OK);
        } else {
            l = dictGetVal(de);
        }
        // 将客户端填接到被阻塞客户端的链表中
        listAddNodeTail(l,c);
    }
    blockClient(c,REDIS_BLOCKED_LIST);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值