【Redis源码剖析】

来源:互联网 发布:js幻灯片切换效果代码 编辑:程序博客网 时间:2024/05/29 19:34

今天为大家带来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的源码就分析到这里。按照惯例,最后提供一份注释的代码供大家参考:传送门。




原创粉丝点击