原创作品,转载请标明:http://blog.csdn.net/Xiejingfa/article/details/51166709
今天为大家带来Redis五大数据类型之一 – List的源码分析。
Redis中的List类型是一种双向链表结构,主要支持以下几种命令:
List的相关操作主要定义在t_list.c和redis.h文件中。归纳起来,主要有以下几个要点:
在前面一篇文章中我们介绍过List类型主要有两种编码方式:REDIS_ENCODING_ZIPLIST和REDIS_ENCODING_LINKEDLIST。其中REDIS_ENCODING_ZIPLIST编码使用的是压缩列表ziplist,REDIS_ENCODING_LINKEDLIST编码使用的是双向链表list(为了便于区分,我们把它称之为linked list)。默认情况下List使用REDIS_ENCODING_ZIPLIST编码,当满足下面两个条件之一时会转变为REDIS_ENCODING_LINKEDLIST编码:
既然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的执行流程如下:
我们可以看到List的操作基本上就是通过当前使用的底层数据结构来完成的,这些数据结构的基本操作我们以前就分析过,这里就不一一赘述了。
除了上面介绍的listTypePush操作,List还有listTypePop、listTypeLength、listTypeInsert、listTypeEqual、listTypeDelete、listTypeConvert等操作,这些操作的实现和listTypePush类似都是通过底层数据结构来实现,代码简单、直观,大家可以类比学习。
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);
这是需要重点理解的地方!
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命令添加的元素个数。我们暂且把该过程称作为“解除阻塞过程”。
下面我们详细讲解一下“阻塞过程”和“解除阻塞过程”的运行过程。
阻塞操作是由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++;
}
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的源码就分析到这里。按照惯例,最后提供一份注释的代码供大家参考:传送门。