Redis源码剖析 - Redis数据类型之列表List

Posted Fred^_^

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis源码剖析 - Redis数据类型之列表List相关的知识,希望对你有一定的参考价值。

原创作品,转载请标明:http://blog.csdn.net/Xiejingfa/article/details/51166709

Redis源码剖析系列文章汇总:传送门

今天为大家带来Redis五大数据类型之一 – List的源码分析。


Redis中的List类型是一种双向链表结构,主要支持以下几种命令:

  1. lpush、rpush、lpushx、rpushx
  2. lpop、rpop、lrange、ltrim、lrem、rpoplpush
  3. linsert、llen、lindex、lset
  4. blpop、brpop、brpoplpush

List的相关操作主要定义在t_list.c和redis.h文件中。归纳起来,主要有以下几个要点:

1、编码方式

在前面一篇文章中我们介绍过List类型主要有两种编码方式:REDIS_ENCODING_ZIPLIST和REDIS_ENCODING_LINKEDLIST。其中REDIS_ENCODING_ZIPLIST编码使用的是压缩列表ziplist,REDIS_ENCODING_LINKEDLIST编码使用的是双向链表list(为了便于区分,我们把它称之为linked list)。默认情况下List使用REDIS_ENCODING_ZIPLIST编码,当满足下面两个条件之一时会转变为REDIS_ENCODING_LINKEDLIST编码:

  1. 当待添加的新字符串长度超过server.list_max_ziplist_value (默认值为64)时。
  2. ziplist中保存的节点数量超过server.list_max_ziplist_entries(默认值为512)时。

既然List类型有两种底层结构,那么显然t_list.c的主要功能之一就是要在ziplist和linked list这两种结构上维护一份统一的List操作接口,以屏蔽底层的差异。

例如我们来看一下listTypePush的源码:

void listTypePush(robj *subject, robj *value, int where) 
    /* Check if we need to convert the ziplist */
    // 检查是否需要转换编码(REDIS_ENCODING_ZIPLIST => REDIS_ENCODING_LINKEDLIST)
    listTypeTryConversion(subject,value);
    if (subject->encoding == REDIS_ENCODING_ZIPLIST &&
        // list_max_ziplist_entries的默认值为512,如果ziplist中存放的节点数超过该值也需要转换编码
        ziplistLen(subject->ptr) >= server.list_max_ziplist_entries)
            listTypeConvert(subject,REDIS_ENCODING_LINKEDLIST);

    // 分别处理以ziplist和linked list编码的情况
    if (subject->encoding == REDIS_ENCODING_ZIPLIST) 
        /* 处理底层结构为ziplist的情况 */

        // 确定新元素是插入到头部还是尾部
        int pos = (where == REDIS_HEAD) ? ZIPLIST_HEAD : ZIPLIST_TAIL;
        value = getDecodedObject(value);
        // 直接调用ziplist的内部函数实现插入操作
        subject->ptr = ziplistPush(subject->ptr,value->ptr,sdslen(value->ptr),pos);
        decrRefCount(value);
     else if (subject->encoding == REDIS_ENCODING_LINKEDLIST) 
        /* 下面处理底层结构为linked list的情况 */
        if (where == REDIS_HEAD) 
            listAddNodeHead(subject->ptr,value);
         else 
            listAddNodeTail(subject->ptr,value);
        
        incrRefCount(value);
     else 
        redisPanic("Unknown list encoding");
    

listTypePush函数的作用是往List类型对象中添加一个元素,其中参数where用于指定添加到表头还是表尾。listTypePush的执行流程如下:

  1. 判断新添加的值的长度是否超过server.list_max_ziplist_value,如果超过则需要转换编码方式。
  2. 如果List的当前编码为REDIS_ENCODING_ZIPLIST方式,判断其保存的节点数量是否超过server.list_max_ziplist_entries,如果超过则需要编码转换。
  3. 判断List的当前编码,如果是REDIS_ENCODING_ZIPLIST,则调用ziplist的内部函数来实现添加操作。如果是REDIS_ENCODING_LINKEDLIST则调用linked list的内部函数实现添加操作。

我们可以看到List的操作基本上就是通过当前使用的底层数据结构来完成的,这些数据结构的基本操作我们以前就分析过,这里就不一一赘述了。

除了上面介绍的listTypePush操作,List还有listTypePop、listTypeLength、listTypeInsert、listTypeEqual、listTypeDelete、listTypeConvert等操作,这些操作的实现和listTypePush类似都是通过底层数据结构来实现,代码简单、直观,大家可以类比学习。

2、迭代器实现

Redis为List类型封装了一个简单的迭代器结构体,定义在redis.h文件中:

/* List类型迭代器结构体 */
typedef struct 
    // 原listType对象
    robj *subject;
    // 编码方式
    unsigned char encoding;
    // 迭代方向
    unsigned char direction; /* Iteration direction */
    // ziplist迭代器
    unsigned char *zi;
    // linked list迭代器
    listNode *ln;
 listTypeIterator;

同时还定义了迭代器节点:

/* List类型节点定义 */
typedef struct 
    listTypeIterator *li;
    unsigned char *zi;  /* Entry in ziplist */
    listNode *ln;       /* Entry in linked list */
 listTypeEntry;

实际上,这listTypeIterator就是将ziplist和linke list的迭代器包装在一起来进一步屏蔽着两种编码方式的区别。与迭代器相关的操作主要有以下几个:

// 创建并返回一个列表迭代器
listTypeIterator *listTypeInitIterator(robj *subject, long index, unsigned char direction);
// 释放listType的迭代器
void listTypeReleaseIterator(listTypeIterator *li);
// 迭代到下一个节点
int listTypeNext(listTypeIterator *li, listTypeEntry *entry);
// 返回当前listTypeEntry结构所保存的节点
robj *listTypeGet(listTypeEntry *entry);

3、阻塞操作

这是需要重点理解的地方!

Redis中有三个阻塞命令blpop、brpop和brpoplpush,这些命令可能会造成客户端被阻塞。接下来我们以blpop命令为例子讲解一下阻塞版的lpop命令是如何运行的。

(1)、如果用户执行BLPOP命令,且指定List不为空,那么程序就直接调用非阻塞的LPOP命令(所以blpop、brpop和brpoplpush只是有可能造成客户端阻塞)。
(2)、如果用户执行BLPOP命令,且指定List为空,这时需要阻塞操作。Redis将相应客户端的状态设置为“阻塞”状态,同时将该客户端添加到db->blocking_keys中。db->blocking_keys是一个字典结构,它的key为被阻塞的键,它的value是一个保存被阻塞客户端的列表。我们暂且把该过程称之为“阻塞过程”
(3)、随后如果有PUSH命令往被阻塞的键中添加元素时,Redis将这个键标识为ready状态。当这个命令执行完毕后,Redis会按照先阻塞先服务的顺序将列表的元素返回给被阻塞的客户端,并且解除阻塞状态的客户端数量取决于PUSH命令添加的元素个数。我们暂且把该过程称作为“解除阻塞过程”

下面我们详细讲解一下“阻塞过程”和“解除阻塞过程”的运行过程。

3.1、阻塞过程

阻塞操作是由blockForKeys函数完成的,函数原型如下:

void blockForKeys(redisClient *c, robj **keys, int numkeys, time_t timeout, robj *target)

blockForKeys函数用于设置客户端对指定键的阻塞状态。参数keys可以指定任意数量的键,timeout指定超时时间,参数target即目标List对象,主要用于brpoplpush命令,用户存放从源列表中pop出来的值。 该函数完成了以下步骤:

(1)、设置阻塞超时时间timeout和目标选项target。
(2)、将客户端信息记录在在c->db->blocking_keys结构中。前面我们说过b->blocking_keys是一个字典结构,它的key为被阻塞的键,它的value是一个保存被阻塞客户端的列表。我们看到blocking_keys定义在redisClient->redisDb结构中,为了方便观察,我省略了其它无关代码:

typedef struct redisDb 
    ...
    dict *blocking_keys;        /* Keys with clients waiting for data (BLPOP) */
    ...
 redisDb;

所以整个结构是这样的:

(3)、将客户端设置为“阻塞”状态。

blockForKeys的源码如下:

/* Set a client in blocking mode for the specified key, with the specified
 * timeout */
/* 设置客户端对指定键的阻塞状态。参数keys可以指定任意数量的键,timeout指定超时时间,参数target即目标listType对象,
    主要用于brpoplpush命令,用户存放从源列表中pop出来的值。 */
void blockForKeys(redisClient *c, robj **keys, int numkeys, time_t timeout, robj *target) 
    dictEntry *de;
    list *l;
    int j;

    // 设置阻塞超时时间 
    c->bpop.timeout = timeout;
    // 设置目标选项,主要用于brpoplpush命令
    c->bpop.target = target;

    // target之拥入rpoplpush命令
    if (target != NULL) incrRefCount(target);

    // 在c->db->blocking_keys添加阻塞客户端和键的映射关系
    for (j = 0; j < numkeys; j++) 
        /* If the key already exists in the dict ignore it. */
        // bpop.keys记录所有阻塞的键
        if (dictAdd(c->bpop.keys,keys[j],NULL) != DICT_OK) continue;
        incrRefCount(keys[j]);

        /* And in the other "side", to map keys -> clients */
        // 维护阻塞键和被阻塞客户端的映射关系
        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);
    

    /* Mark the client as a blocked client */
    // 将客户端设置为“阻塞”状态
    c->flags |= REDIS_BLOCKED;
    server.bpop_blocked_clients++;

3.2、解除阻塞过程

List的阻塞解除过程如下:

(1)、 如果有其它客户端执行命令往该key(即List)添加新值,先在blocking_keys中检查是否有客户端因该key而被阻塞,如果有则调用signalListAsReady为该key创建一个readyList结构并放入server.ready_keys链表中,同时也将该key添加到db->ready_keys中。db->ready_keys是一个哈希表,它的value为NULL。这个server.ready_keys列表最后会handleClientsBlockedOnLists函数处理。

这里有一个注意点:为什么要用一个链表和一个哈希表来存储同一个key?如果往一个key中添加了多个新值,Redis只需要往server.ready_keys为该key保存一个相关的readyList节点即可,这样可以避免在一个事务或脚本中将同一个key一次又一次地添加到server.ready_keys列表中。为了不重复添加,每次执行添加查找前需要进行一次“查重”操作,但是server.ready_keys是一个链表,在其中进行查找操作时间复杂度为O(n),效率比较差。为解决这个问题Redis引入了db->ready_keys哈希表结构来保存同一个key,哈希表的查找查找效率高,所以每次往server.ready_keys添加节点时候只要在db->ready_keys检查一下就知道server.ready_keys有没有相同的节点了。

下面我们来看看signalListAsReady函数涉及到的结构体:

readyList定义在redis.h文件中:

typedef struct readyList 
    // key所在的数据库
    redisDb *db;
    // 造成阻塞的键
    robj *key;
 readyList;

readyList结构表示server.ready_keys链表中的一个节点,其中key字段表示阻塞的key,db指向该键所在的数据库。

db->ready_keys定义在redisDb结构体中,用于存放已经准备好数据的阻塞状态的key:

typedef struct redisDb 
    ...
    dict *ready_keys;           /* Blocked keys that received a PUSH */
    ...
 redisDb;

signalListAsReady函数的源码如下:

void signalListAsReady(redisDb *db, robj *key) 
    // readyList定义在redis.h中,表示server.ready_keys的一个节点
    readyList *rl;

    /* No clients blocking for this key? No need to queue it. */
    // 如果没有客户端因这个key而被阻塞,则直接返回
    if (dictFind(db->blocking_keys,key) == NULL) return;

    /* Key was already signaled? No need to queue it again. */
    // 如果这个key已经添加到ready_keys,为避免重复添加直接返回
    if (dictFind(db->ready_keys,key) != NULL) return;

    /* Ok, we need to queue this key into server.ready_keys. */
    // 创建一个readyList结构,然后添加到server.ready_keys尾部
    rl = zmalloc(sizeof(*rl));
    rl->key = key;
    rl->db = db;
    incrRefCount(key);
    listAddNodeTail(server.ready_keys,rl);

    /* We also add the key in the db->ready_keys dictionary in order
     * to avoid adding it multiple times into a list with a simple O(1)
     * check. */
    incrRefCount(key);
    // 将key添加到db->ready_keys中,避免重复添加
    redisAssert(dictAdd(db->ready_keys,key,NULL) == DICT_OK);

到目前为止,Redis只是收集好了已经准备好数据的处于阻塞状态的key信息,接下来才是真正解除客户端阻塞状态的操作。

(2)、调用handleClientsBlockedOnLists函数,该函数将遍历server.ready_keys中已经准备好数据的key,同时遍历阻塞在该key上的所有客户端(直接从c->db->blocking_keys地点中获取客户端列表)。如果key不为空则从key中弹出一个元素返回给客户端并解除客户端的阻塞状态直到该key为空或没有客户端因为该key而阻塞为止。

handleClientsBlockedOnLists函数的源码如下,代码也很简单。

void handleClientsBlockedOnLists(void) 
    // 遍历server.ready_keys列表
    while(listLength(server.ready_keys) != 0) 
        list *l;

        /* Point server.ready_keys to a fresh list and save the current one
         * locally. This way as we run the old list we are free to call
         * signalListAsReady() that may push new elements in server.ready_keys
         * when handling clients blocked into BRPOPLPUSH. */
        // 备份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);
            readyList *rl = ln->value;

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

            /* If the key exists and it's a list, serve blocked clients
             * with data. */
            // 获取listType对象
            robj *o = lookupKeyWrite(rl->db,rl->key);
            if (o != NULL && o->type == REDIS_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);
                        redisClient *receiver = clientnode->value;
                        // 设置pop出的目标对象
                        robj *dstkey = receiver->bpop.target;
                        // 从列表中弹出对象
                        int where = (receiver->lastcmd &&
                                     receiver->lastcmd->proc == blpopCommand) ?
                                    REDIS_HEAD : REDIS_TAIL;
                        robj *value = listTypePop(o,where);

                        // 如果listType还有元素,返回给相应客户端
                        if (value) 
                            /* Protect receiver->bpop.target, that will be
                             * freed by the next unblockClientWaitingData()
                             * call. */
                            if (dstkey) incrRefCount(dstkey);
                            // 解除相应客户端的阻塞状态
                            unblockClientWaitingData(receiver);

                            // 将pop出来的值返回给相应的客户端receiver
                            if (serveClientBlockedOnList(receiver,
                                rl->key,dstkey,rl->db,value,
                                where) == REDIS_ERR)
                            
                                /* If we failed serving the client we need
                                 * to also undo the POP operation. */
                                // 如果操作失败,则回滚(插入原listType对象)
                                    listTypePush(o,value,where);
                            

                            if (dstkey) decrRefCount(dstkey);
                            decrRefCount(value);
                         else 
                            // 如果listType中没有元素了,没有元素可以返回剩余被阻塞客户端,只能等待以后的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的源码就分析到这里。按照惯例,最后提供一份注释的代码供大家参考:传送门

以上是关于Redis源码剖析 - Redis数据类型之列表List的主要内容,如果未能解决你的问题,请参考以下文章

Redis源码剖析 - Redis数据类型之有序集合zset

Redis源码剖析 - Redis之数据库redisDb

Redis源码剖析--对象object

Redis源码剖析--对象object

Redis源码剖析--对象系统

Redis源码剖析--字符串t_string