Redis源码剖析--列表list

2023-05-16

上一篇博客Redis源码剖析–快速列表 带大家一起剖析了quicklist这个底层数据结构的实现原理。Redis对外开放的列表list结构就是采用quicklist作为底层实现(在新版本的Redis源码中,不再采用ziplist和sdlist两种结构,而是统一采用quicklist)。有关列表键的实现源码在t_list.c文件中,大家可以边看源码边看这篇博客,一起来理解。

List概述

其实在[Redis源码剖析—对象Object]一文中有一个错误,list数据类型的底层编码并没有采用ziplist和sdlist,而是统一采用quicklist作为底层数据结构,这点需要提前说明一下。Redis的新版本中,list的底层编码类型只有OBJ_ENCODING_QUICKLIST,那么原先关于合适进行编码类型转换的代码都省略了。

列表没有其特有的数据结构,而是采用RedisObject作为其泛型数据结构,当RedisObject的编码类型为OBJ_LIST时,该对象被认为是一个列表。

Redis为列表提供了迭代器结构,本质就是quicklist迭代器的基本上做了一层封装。

typedef struct {
    robj *subject;  // 迭代器指向的对象
    unsigned char encoding;  // 编码类型
    unsigned char direction; // 迭代器方向
    quicklistIter *iter; // quicklist的迭代器
} listTypeIterator;
// 代表list中的某个数据项
typedef struct {
    listTypeIterator *li;  // list迭代器指针
    quicklistEntry entry; // quicklist的数据项节点结构
} listTypeEntry;

List主要接口

列表定义了基本的接口函数,包括push,pop,insert,find等等,基本上都是在quicklist上做了一次封装。我们先来看看主要有哪些接口。

// list的push操作
void listTypePush(robj *subject, robj *value, int where);
// list的pop操作
robj *listTypePop(robj *subject, int where);
// 返回list的数据项个数总和
unsigned long listTypeLength(robj *subject);
// 初始化一个list迭代器
listTypeIterator *listTypeInitIterator(robj *subject, long index,
                                       unsigned char direction);
// 释放一个list迭代器
void listTypeReleaseIterator(listTypeIterator *li);
// 指向下一个数据项的list迭代器
int listTypeNext(listTypeIterator *li, listTypeEntry *entry);
// 返回entry指向的list中的数据项的值
robj *listTypeGet(listTypeEntry *entry) ;
// 在entry指向的list数据项前面或者后面插入value
void listTypeInsert(listTypeEntry *entry, robj *value, int where);
// 比较entry指向的list中的数据项与o的大小
int listTypeEqual(listTypeEntry *entry, robj *o);
// 删除entry指向的list中的数据项
void listTypeDelete(listTypeIterator *iter, listTypeEntry *entry);
// 将OBJ_ENCODING_ZIPLIST类型编码的列表转换成OBJ_ENCODING_QUICKLIST编码的列表
void listTypeConvert(robj *subject, int enc);

其中,我们以push和pop操作来简要看看这些函数的实现源码。

// 向list中压入数据
void listTypePush(robj *subject, robj *value, int where) {
    // 仅仅当编码类型为OBJ_ENCODING_QUICKLIST时才进行操作
    if (subject->encoding == OBJ_ENCODING_QUICKLIST) {
        // 判断压入位置
        int pos = (where == LIST_HEAD) ? QUICKLIST_HEAD : QUICKLIST_TAIL;
        // 从value中解码出数据项
        value = getDecodedObject(value);
        // 得到数据项的长度
        size_t len = sdslen(value->ptr);
        // 调用quicklistPush插入数据
        quicklistPush(subject->ptr, value->ptr, len, pos);
        // 将数据项对象的引用次数减1,也就是释放value
        decrRefCount(value);
    } else {
        serverPanic("Unknown list encoding");
    }
}
// 向list中弹出数据
robj *listTypePop(robj *subject, int where) {
    long long vlong;
    robj *value = NULL;
    // 判断弹出位置
    int ql_where = where == LIST_HEAD ? QUICKLIST_HEAD : QUICKLIST_TAIL;
    // 仅仅当编码类型为OBJ_ENCODING_QUICKLIST时才进行操作
    if (subject->encoding == OBJ_ENCODING_QUICKLIST) {
        // 调用quicklistPopCustom函数弹出数据
        if (quicklistPopCustom(subject->ptr, ql_where, (unsigned char **)&value,
                               NULL, &vlong, listPopSaver)) {
            if (!value)
                // 将数据项编码成string类型的RedisObject
                value = createStringObjectFromLongLong(vlong);
        }
    } else {
        serverPanic("Unknown list encoding");
    }
    // 返回string类型编码的数据项对象
    return value;
}

其他的一些接口函数均是调用quicklist提供的底层接口函数来实现,大家有空可以对照源码来看看。

List命令

与string一样,list也提供了很多命令以供用户使用。按照惯例,先列一张表给大家(包含部分重要指令)。

命令形式命令描述
LPUSH key value1 [value2….]将一个或多个值插入到列表头部
LPOP key移除并获取列表的头部第一个元素
RPUSH key value1 [value2….]将一个或者多个值插入到列表尾部
RPOP key移除并获取列表的尾部第一个元素
LPUSHX key value为已存在的列表头部添加值
RPUSHX key value为已存在的列表尾部添加值
BLPOP key1 [key2…] timeout移出并获取列表的第一个元素(阻塞模式)
BRPOP key1 [key2…] timeout移出并获取列表的最后一个元素(阻塞模式)
BRPOPLPUSH source destination timeout从列表中弹出一个值,将弹出的元素插入到另外一个列表中并返回它(阻塞模式)
LLEN key获取列表长度
LINDEX通过索引获取列表中的元素

同样,博主仅仅贴出部分源码来供大家理解这些命令的简要实现过程,我们来看看LPUSH和RPUSH命令的实现。

// lpush操作
void lpushCommand(client *c) {
    c->argv[2] = tryObjectEncoding(c->argv[2]);
    pushxGenericCommand(c,NULL,c->argv[2],LIST_HEAD);
}
// rpush操作
void rpushCommand(client *c) {
    c->argv[2] = tryObjectEncoding(c->argv[2]);
    pushxGenericCommand(c,NULL,c->argv[2],LIST_TAIL);
}
// 真正的push操作函数,where指定位置
void pushGenericCommand(client *c, int where) {
    int j, waiting = 0, pushed = 0;
    // 在数据库中查找是否存在该键,如果存在则返回该键,反之返回NULL
    robj *lobj = lookupKeyWrite(c->db,c->argv[1]);
    // 如果该键并非list,属于类型错误,交由服务器处理
    if (lobj && lobj->type != OBJ_LIST) {
        addReply(c,shared.wrongtypeerr);
        return;
    }
    // 添加数据元素
    for (j = 2; j < c->argc; j++) {
        // 试图将该元素编码成字符串类型以节省空间
        c->argv[j] = tryObjectEncoding(c->argv[j]);
        // 如果该列表不存在
        if (!lobj) {
            // 创建一个编码类型为OBJ_ENCODING_QUICKLIST的列表
            lobj = createQuicklistObject();
            // 设定列表的属性
            quicklistSetOptions(lobj->ptr, server.list_max_ziplist_size,
                                server.list_compress_depth);
            // 将键和新的列表作为键值对添加到数据库
            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 == LIST_HEAD) ? "lpush" : "rpush";
        // 发送键修改信号
        signalModifiedKey(c->db,c->argv[1]);
        // 发送事件通知
        notifyKeyspaceEvent(NOTIFY_LIST,event,c->argv[1],c->db->id);
    }
    // 服务器的脏数据个数增加
    server.dirty += pushed;
}

这些命令的源码实现基本上大同小异,不过相对于其他数据类型,list提供了带有阻塞的命令,包括BLPOP,BRPOP,BLPOPRPUSH,这些命令可能会造成客户端被阻塞。这属于list的一大特色,也是需要着重理解的地方。

阻塞命令

前面提到,list为用户提供了三个带有阻塞模式的命令,分别是BLPOP,BRPOP,BLPOPRPUSH。那么,到底这些命令是如何执行,如何进行阻塞和解阻塞的呢?首先,我们来看看BLPOP,BRPOP的源码。

// BLPOP命令
void blpopCommand(client *c) {
    blockingPopGenericCommand(c,LIST_HEAD);
}
// BRPOP命令
void brpopCommand(client *c) {
    blockingPopGenericCommand(c,LIST_TAIL);
}
// 带有阻塞的pop命令实现函数
void blockingPopGenericCommand(client *c, int where) {
    robj *o;
    mstime_t timeout;
    int j;
    // 取出timeout参数
    if (getTimeoutFromObjectOrReply(c,c->argv[c->argc-1],&timeout,UNIT_SECONDS)
        != C_OK) return;
    // 遍历所有输入键
    for (j = 1; j < c->argc-1; j++) {
        // 在当前数据库中查找list键
        o = lookupKeyWrite(c->db,c->argv[j]);
        if (o != NULL) {
            // 执行到此处,说明数据库中存在此键
            // 检查类型
            if (o->type != OBJ_LIST) {
                addReply(c,shared.wrongtypeerr);
                return;
            } else {
                // list不为空的话,则转换为普通的pop操作
                if (listTypeLength(o) != 0) {
                    // 当前list不为空,转换为普通的pop进行处理
                    char *event = (where == LIST_HEAD) ? "lpop" : "rpop";
                    robj *value = listTypePop(o,where);
                    serverAssert(value != NULL);

                    addReplyMultiBulkLen(c,2);
                    addReplyBulk(c,c->argv[j]);
                    addReplyBulk(c,value);
                    decrRefCount(value);
                    notifyKeyspaceEvent(NOTIFY_LIST,event,
                                        c->argv[j],c->db->id);
                    // 如果弹出后list为空,则删除
                    if (listTypeLength(o) == 0) {
                        dbDelete(c->db,c->argv[j]);
                        notifyKeyspaceEvent(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. */
                    rewriteClientCommandVector(c,2,
                        (where == LIST_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 & CLIENT_MULTI) {
        addReply(c,shared.nullmultibulk);
        return;
    }

    // 执行到此处,说明列表为空,或者当前键并不存在
    // 执行阻塞
    blockForKeys(c, c->argv + 1, c->argc - 2, timeout, NULL);
}

从这段代码中,我们可以看出,当执行带有阻塞的pop命令时,有如下两种情况。

  • 如果指定的list存在于当前数据库中且list不为空,则转而执行普通的pop操作
  • 如果指定的list键不存在,或者该list为空,则执行阻塞操作

那么阻塞的过程是如下进行的呢?别急,我们去查看以下blockForKeys函数,看看它干了些什么。

// 设置客户端对指定键的阻塞状态
// 参数keys可以指定任意数量的键,timeout指定超时时间,target代表目标listType对象
void blockForKeys(client *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++) {
        // 如果当前键存在,则忽略;反之则添加该键
        // bpop.keys记录所有造成客户端阻塞的键
        if (dictAdd(c->bpop.keys,keys[j],NULL) != DICT_OK) continue;
        incrRefCount(keys[j]);

        // blocking_keys是一个字典,其键为造成阻塞的键,值是一个链表,记录所有被该键阻塞的客户端
        // 查找当前造成阻塞的键
        de = dictFind(c->db->blocking_keys,keys[j]);
        if (de == NULL) {
            // 键不存在,则新创建一个,并将它关联到字典中
            int retval;
            // 创建新的list
            l = listCreate();
            // 将键和值加入到c->db->blocking_keys中
            retval = dictAdd(c->db->blocking_keys,keys[j],l);
            incrRefCount(keys[j]);
            serverAssertWithInfo(c,keys[j],retval == DICT_OK);
        } else {
            // 如果键存在,则直接获取该键的值
            l = dictGetVal(de);
        }
        // 将客户端加入到链表中
        listAddNodeTail(l,c);
    }
    // 阻塞该客户端
    blockClient(c,BLOCKED_LIST);
}

在上述代码中,设计到server.c中的一些数据结构。这里我简要的罗列一下。

typedef struct client {
    redisDb *db;   // 指向当前数据库
    blockingState bpop;  // 记录阻塞状态
    // ...其他的参数省略
}
// 阻塞状态结构体
typedef struct blockingState {
    mstime_t timeout;      // 阻塞超时时间

    dict *keys;           // 记录所有造成客户端阻塞的键
    robj *target;         // 目标选项,target在执行RPOPLPUSH命令时使用,

    /* BLOCKED_WAIT */
    int numreplicas;        /* Number of replicas we are waiting for ACK. */
    long long reploffset;   /* Replication offset to reach. */
} blockingState;
typedef struct redisDb {
    dict *blocking_keys;        // 记录所有造成阻塞的键,及其相应的客户端
    // ...其他参数省略
} redisDb;

Redis采用了一个字典结构blocking_keys,其将所有造成阻塞的键,以及阻塞于该键的所有客户端的信息存放起来。执行完这些以后,就调用blockClient函数,真正的对该客户端进行阻塞。

那么,接下来要考虑的问题就是如何解阻塞,客户端不可能一直阻塞在那吧,是不是?由我们之前设定的参数,可以推测出来,有两种情况会对客户端进行解阻塞操作。

  • 执行阻塞的时候,设置了超时参数,如果阻塞时长超过了该参数设定的时间,则自动对该客户端进行解阻塞
  • 执行阻塞的时候,记录了所有造成客户端阻塞的键,那么如果有其他客户端执行命令,往造成阻塞的键里面添加了新值,这个时候Redis检查到该键中有值了,就会处理pop命令,也就是说,Redis采用先阻塞,后执行的策略来执行阻塞命令。

有了这些推测之后,我们就去push命令中找关于解阻塞的操作,一番查找之后,锁定了signalListAsReady函数,该函数在dbadd函数中执行。于是,跳转到signalListAsReady函数的源码。

// 如果有客户端因为等待给定key 被push阻塞,那么将此key加入到server.ready_keys中
// 这个列表最终会被 handleClientsBlockedOnLists() 函数处理。
void signalListAsReady(redisDb *db, robj *key) {
    readyList *rl;

    // 如果在所有造成客户端阻塞的键中找不到此键,则不作处理
    if (dictFind(db->blocking_keys,key) == NULL) return;

    // 这个键已经存在于ready_keys中了,则不做处理
    if (dictFind(db->ready_keys,key) != NULL) return;

    // 执行到此,说明有客户端因为此键被阻塞,且此键不存在于db->ready_keys中
    // 创建一个新的readylists结构,保存键和数据库
    // 然后将该结构添加到server.ready_keys中
    rl = zmalloc(sizeof(*rl));
    rl->key = key;
    rl->db = db;
    incrRefCount(key);
    listAddNodeTail(server.ready_keys,rl);

    // 同样,将key添加到db->ready_keys中
    incrRefCount(key);
    serverAssert(dictAdd(db->ready_keys,key,NULL) == DICT_OK);
}

此代码中有一点小小的疑惑,db->ready_keysserver.ready_keys这不重复了吗?为什么要设计这两个同样的结构。于是我们来查看以下它们的定义。

typedef struct redisDb {
    dict *ready_keys;           // 存放push操作添加的造成阻塞的键,字典结构
    // 省略了其他参数
} redisDb;

struct redisServer {
    list *ready_keys;    // 存在push操作添加的造成阻塞的键,链表结构
    // 省略了不必要的参数
}
// ready_keys链表结构中存放的节点数据结构
typedef struct readyList {
    redisDb *db;  // key所在的数据库
    robj *key;  //造成阻塞的键
} readyList;

Redis采用了一个链表和一个字典结构存放同一个key,想了想,这似乎也有道理。假设我们往一个key中添加多个新值时,Redis只需要在server.ready_keys中为该key保存一个readyList节点即可,这样可以避免在一个事务或者脚本中将同一个key一次又一次的添加到server.ready_keys中,为了避免不重复添加,Redis又采用一个链表结构db->ready_keys来快速判断server.ready_keys中是否存在该键。这样一来,既保证了不重复添加,又保证了哈希结构带来的查找效率。

好了,理解了这一点,我们继续往下剖析,在push操作的时候,只是回收了push进来的造成阻塞的键,如何利用这些信息对已经阻塞的客户端进行解阻塞呢?Redis在运行的过程中,会一直查看server.ready_keys里是否有值,如果有则需要对存放的值对应的客户端进行接阻塞,此操作由handleClientsBlockedOnLists函数执行。

// 遍历server.ready_keys中所有已经准备好的key,同时在c->db->blocking_keys中
// 遍历所有由此键造成阻塞的客户端,如果key不为空的话,就从key中弹出一个元素返回给客户端并
// 接触该客户端的阻塞状态,直到server.ready_keys为空,或没有因该key而阻塞的客户端为止
void handleClientsBlockedOnLists(void) {
    while(listLength(server.ready_keys) != 0) {
        list *l;

        // 备份server.ready_keys,然后初始化server.ready_keys
        l = server.ready_keys;
        server.ready_keys = listCreate();
        // 不为空
        while(listLength(l) != 0) {
            // 取出server.ready_keys中的第一个节点
            listNode *ln = listFirst(l);
            // 指向redislist结构
            readyList *rl = ln->value;

            /* First of all remove this key from db->ready_keys so that
             * we can safely call signalListAsReady() against this key. */
            // 从ready_keys中移除就绪的key
            dictDelete(rl->db->ready_keys,rl->key);

            /* If the key exists and it's a list, serve blocked clients
             * with data. */
            // 获取键对象,此对象非空且为list结构
            robj *o = lookupKeyWrite(rl->db,rl->key);
            if (o != NULL && o->type == OBJ_LIST) {
                dictEntry *de;

                /* We serve clients in the same order they blocked for
                 * this key, from the first blocked to the last. */
                // 取出没有被这个key阻塞的客户端
                de = dictFind(rl->db->blocking_keys,rl->key);
                if (de) {
                    list *clients = dictGetVal(de);
                    int numclients = listLength(clients);

                    while(numclients--) {
                        // 取出客户端
                        listNode *clientnode = listFirst(clients);
                        client *receiver = clientnode->value;
                        // 设置弹出的目标对象(只在 BRPOPLPUSH 时使用)
                        robj *dstkey = receiver->bpop.target;
                        // 从列表中弹出元素
                        // 弹出的位置取决于是执行 BLPOP 还是 BRPOP 或者 BRPOPLPUSH
                        int where = (receiver->lastcmd &&
                                     receiver->lastcmd->proc == blpopCommand) ?
                                    LIST_HEAD : LIST_TAIL;
                        robj *value = listTypePop(o,where);
                        // 还有元素可弹出,非NULL
                        if (value) {
                            /* Protect receiver->bpop.target, that will be
                             * freed by the next unblockClient()
                             * call. */
                            if (dstkey) incrRefCount(dstkey);
                            // 取消客户端的阻塞状态
                            unblockClient(receiver);

                            if (serveClientBlockedOnList(receiver,
                                rl->key,dstkey,rl->db,value,
                                where) == C_ERR)
                            {
                                /* If we failed serving the client we need
                                 * to also undo the POP operation. */
                                    listTypePush(o,value,where);
                            }

                            if (dstkey) decrRefCount(dstkey);
                            decrRefCount(value);
                        } else {
                            // 执行到此处,表示还有至少一个客户端被该key阻塞
                            // 这些客户端需要下一次push才能被解阻塞
                            break;
                        }
                    }
                }
                // 如果列表元素已经为空,那么从数据库中将它删除
                if (listTypeLength(o) == 0) {
                    dbDelete(rl->db,rl->key);
                }
                /* We don't call signalModifiedKey() as it was already called
                 * when an element was pushed on the list. */
            }

            /* Free this item. */
            // 释放
            decrRefCount(rl->key);
            zfree(rl);
            listDelNode(l,ln);
        }
        listRelease(l); /* We have the new list on place at this point. */
    }
}

剖析到此,整个阻塞操作的流程就都清晰明了了。如有疑惑,可以在留言区留言,咋们继续讨论。

List小结

本篇博客剖析list的主要接口,以及所有命令的实现,值得大家注意的是带阻塞的pop命令,这个在上文中有详细的实现过程,分析源码的过程就向探索迷宫一样,一步一步的把它藏在深处的秘密挖出来,坚持下去总会有收获。keep moving!明天继续按照预定的步骤分析!

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Redis源码剖析--列表list 的相关文章

随机推荐

  • OracleRAC管理 之 集群状态&信息查看

    OracleRAC管理 之 集群状态 amp 信息 查看 by 王磊 菜小小 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61
  • fgetc,fgets,getline用法

    Linux Ubuntu 下用法 在Ubuntu下shell中 xff0c man fgets可以看到fgetc fgets等用法 xff0c man getline可以看到getline用法 span class hljs comment
  • 使用PyQt4制作一个音乐播放器(1)

    1 前言 最近用Python给老妈写了一个处理excel表格的小软件 xff0c 因为自己平时用Python一般都是用在数值计算领域 xff0c 所以一般使用命令行的方式交互即可 但是给老妈用发现用命令行交互方式使用并不是很方便 xff0c
  • ubuntu18.04/16.04 忘记开机密码,咋办?

    自己的 笔记本 装了双系统 xff0c 很久没有ubuntu系统了 xff0c 密码竟然给忘记了 于是乎 xff0c 在网上找答案 精髓 大概就是在句话 xff1a recovery nomodeset 并将之删除 xff0c 然后下移一行
  • C++ 实现阻塞队列

    文章出处 xff0c 来源自地址 xff1a C 43 43 实现阻塞队列 文章 详细代码 xff1a include lt queue gt include lt thread gt include lt mutex gt include
  • 浏览器请求nodejs搭建的web服务器上的html文件时引用不了jQuery.js文件解决办法

    构建web服务器代码 nodejs构建 span class token comment 引入核心模块http和fs span span class token keyword var span http span class token
  • AI 到底是怎么「想」的?

    本文作者 xff1a kurffzhou xff0c 腾讯 TEG 安全工程师 最近 xff0c Nature发表了一篇关于深度学习系统被欺骗的新闻文章 xff0c 该文指出了对抗样本存在的广泛性和深度学习的脆弱性 xff0c 以及几种可能
  • kafka3.1 docker-compose方式安装(二)

    ZooKeeper 是为 Kafka 提供协调服务的工具 xff0c 所以得启动一个ZooKeeper 容器 配置文件 docker compose yml span class token key atrule version span
  • 效能优化实践:C/C++单元测试万能插桩工具

    作者 xff1a mannywang xff0c 腾讯安全平台后台开发 研发效能是一个涉及面很广的话题 xff0c 它涵盖了软件交付的整个生命周期 xff0c 涉及产品 架构 开发 测试 运维 xff0c 每个环节都可能影响顺畅 高质量地持
  • Windows使用cmd刷入recovery.img

    Windows使用cmd刷入recovery img Windows键 43 R回车后进入cmd命令终端 进入fastboot 手机进入fastboot模式有2种方法 第一种进入方法是 xff0c 如果你的手机能用adb识别到 xff0c
  • Linux系统安装环境桌面

    最近工作加班还有在弄大数据 xff0c 有段时间没有记录了 xff0c 后续有空的话整理下大数据的东西分享 xff0c 今天记录下Linux安装环境桌面 之前弄了个阿里云服务器玩玩 xff0c CentOS 7的版本 xff0c CentO
  • 【Linux学习笔记】关于ubuntu开机菜单栏和任务栏不见了的有效解决方法

    一 问题描述 ubuntu开机只有桌面 xff0c 没有菜单栏和任务栏 xff0c 如下图 xff1a 二 问题解决 刚学习ubuntu xff0c 总有些像我这样不折腾就不舒服的人 xff0c 今天改了一下主题 xff0c 图标什么的 x
  • 【数据结构与算法】深入浅出递归和迭代的通用转换思想

    深入浅出递归和迭代的通用转换思想 一般来说 xff0c 能用迭代的地方就不要用递归 xff01 理论上讲 xff0c 所有的递归和迭代之间都能相互转换 xff01 刷题碰到 一天一道LeetCode 130 Surrounded Regio
  • 【unix网络编程第三版】阅读笔记(二):套接字编程简介

    unp第二章主要将了TCP和UDP的简介 xff0c 这些在 TCP IP详解 和 计算机网络 等书中有很多细致的讲解 xff0c 可以参考本人的这篇博客 计算机网络 第五版 阅读笔记之五 xff1a 运输层 xff0c 这篇博客就不再赘述
  • 带你深入理解STL之Deque容器

    在介绍STL的deque的容器之前 xff0c 我们先来总结一下vector和list的优缺点 vector在内存中是分配一段连续的内存空间进行存储 xff0c 其迭代器采用原生指针即可 xff0c 因此其支持随机访问和存储 xff0c 支
  • 带你深入理解STL之Set和Map

    在上一篇博客 带你深入理解STL之RBTree中 xff0c 讲到了STL中关于红黑树的实现 xff0c 理解起来比较复杂 xff0c 正所谓前人种树 xff0c 后人乘凉 xff0c RBTree把树都种好了 xff0c 接下来就该set
  • Redis源码剖析--字符串t_string

    前面一直在分析Redis的底层数据结构 xff0c Redis利用这些底层结构设计了它面向用户可见的五种数据结构 xff0c 字符串 哈希 xff0c 链表 xff0c 集合和有序集合 xff0c 然后用redisObject对这五种结构进
  • 制作启动盘安装Ubuntu 18.04.1

    1 使用UltraISO制作启动盘 首先 xff0c 下载Ubuntu镜像文件 链接 xff1a https pan baidu com s 1saIJuLzAR5ojc7 F3EQ Q 提取码 xff1a 1hc1 打开UItralISO
  • Redis源码剖析--快速列表quicklist

    在RedisObject这一篇博客中 xff0c 有介绍到list结构的底层编码类型有OBJ ENCODING QUICKLIST xff0c 当时就发现这个底层数据结构被我遗漏了 昨天花了点时间补了补这个知识 xff0c 看完发现这货就跟
  • Redis源码剖析--列表list

    上一篇博客Redis源码剖析 快速列表 带大家一起剖析了quicklist这个底层数据结构的实现原理 Redis对外开放的列表list结构就是采用quicklist作为底层实现 xff08 在新版本的Redis源码中 xff0c 不再采用z