欢迎大家关注 github.com/hsfxuebao ,希望对大家有所帮助,要是觉得可以的话麻烦给点一下Star哈 本文分析双端链表和快速列表两种编码结构。
1. 链表
在 Redis 中 OBJ_ENCODING_LINKEDLIST 这种编码方式对应的数据结构是链表。目前 Redis 已经不用 OBJ_ENCODING_LINKEDLIST 这种编码方式了,但链表这种数据结构在 Redis 中还在继续使用,这里简单介绍一下。
1.1 链表的定义
Redis 实现的链表是一个双向链表,其源码在 src/adlist.h 文件中,其中,链表节点的定义源码如下:
typedef struct listNode {
struct listNode *prev; //前置节点
struct listNode *next; //后置节点
void *value; //节点的值
} listNode;
链表的定义源码如下:
typedef struct list {
listNode *head; //表头节点
listNode *tail; //表尾节点
void *(*dup)(void *ptr); //节点复制函数
void (*free)(void *ptr); //节点释放函数
int (*match)(void *ptr, void *key); //节点值比对函数
unsigned long len; //链表包含的节点数量
} list;
对应的数据结构为:
1.2 链表特性
- 双端: 链表节点带有
prev和next指针,获取某个节点的前置节点和后置节点的复杂度都是 O(1) 。 - 无环: 表头节点的
prev指针和表尾节点的next指针都是指向NULL,对链表的访问以NULL为终点。 - 通过
list结构的head指针和tail指针,获取表头节点和表尾节点的复杂度为 O(1) 。 - 带有链表长度计数器,获取链表长度的复杂度为 O(1) 。
- 多态: 链表节点使用
void *指针来保存各种不同类型的值。
2. 快速列表
在 Redis 的早期设计中,当列表类型的对象中元素的长度较小或者数量比较少的时候,采用压缩列表来存储,反之则会使用双向链表来存储。
双向链表便于在链表的两端进行插入和删除操作,在插入节点上复杂度很低,但是它的内存开销比较大,它在每个节点上除了要保存数据之外,还要额外保存两个指针,并且双向链表的各个节点是单独的内存块,地址不连续,节点多了容易产生内存碎片。
压缩列表存储在一段连续的内存上,所以存储效率很高,但是,它不利于修改操作,插入和删除操作需要频繁的申请和释放内存,特别是当压缩列表长度很长的时候,一次 realloc 可能会导致大批量的数据拷贝。
Redis 在 3.2 版本之后引入了快速列表,列表类型的对象其底层都是由快速列表实现。快速列表是双向链表和压缩列表的混合体,它将双向链表按段切分,每一段使用压缩列表来紧凑存储,多个压缩列表之间使用双向指针串接起来。
quicklist 实际上是 zipList 和 linkedList 的混合体,它将 linkedList按段切分,每一段使用 zipList 来紧凑存储,多个 zipList 之间使用双向指针串接起来。如下图所示:
2.1 数据结构
Redis 中快速列表定义在 src/quicklist.h 文件中,快速列表如下:
typedef struct quicklist {
quicklistNode *head;
quicklistNode *tail;
unsigned long count; /* total count of all entries in all ziplists */
unsigned long len; /* number of quicklistNodes */
int fill : QL_FILL_BITS; /* fill factor for individual nodes */
unsigned int compress : QL_COMP_BITS; /* depth of end nodes not to compress;0=off */
unsigned int bookmark_count: QL_BM_BITS;
quicklistBookmark bookmarks[];
} quicklist;
head:指向头节点(左侧第一个节点)的指针。tail:指向尾节点(右侧第一个节点)的指针。count:所有压缩列表的节点数量之和。len:快速列表节点数量。fill:32 位机器上占 14 bit,64 位机器上占 16 bit,存放list-max-ziplist-size参数的值,用于设置压缩列表节点的大小。compress:32 位机器上占 14 bit,64 位机器上占 16 bit,存放list-compress-depth参数的值,用于设置压缩深度。快速列表默认的压缩深度为 0,即不压缩。为了支持快速的 push/pop 操作,快速列表的首尾两个节点不压缩,此时压缩深度就是 1。如果压缩深度为 2,就表示快速列表的首尾第一个及第二个节点都不压缩。bookmark_count:占 4 bit,bookmarks数组的长度。bookmarks:这是一个可选字段,快速列表重新分配内存时使用,不使用时不占用空间。
快速列表节点如下:
typedef struct quicklistNode {
struct quicklistNode *prev;
struct quicklistNode *next;
unsigned char *zl;
unsigned int sz; /* ziplist size in bytes */
unsigned int count : 16; /* count of items in ziplist */
unsigned int encoding : 2; /* RAW==1 or LZF==2 */
unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */
unsigned int recompress : 1; /* was this node previous compressed? */
unsigned int attempted_compress : 1; /* node can't compress; too small */
unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;
typedef struct quicklistLZF {
//压缩后的压缩列表大小
unsigned int sz; /* LZF size in bytes*/
//压缩后的压缩列表数据
char compressed[];
} quicklistLZF;
prev:指向前一个快速列表节点的指针。next:指向后一个快速列表节点的指针。zl:数据指针,如果当前节点的数据没有压缩,它指向一个ziplist结构,否则,它指向一个quicklistLZF结构。sz:表示zl指向的数据总大小,注意,如果数据被压缩了,其表示压缩前的数据大小。count:占 16 bit,表示当前的快速列表节点中数据项个数。encoding:表示 当前节点的数据是否被压缩,目前只有两种取值:1 表示没有压缩;2 表示被压缩了,且用的时 LZF 压缩算法。container:是一个预留字段,本来设计是用来表明一个quicklist节点下面是直接存数据,还是使用ziplist存数据,或者用其它的结构来存数据(用作一个数据容器,所以叫container)。但是,在目前的实现中,这个值是一个固定的值2,表示使用 ziplist 作为数据容器。recompress:当我们使用类似lindex这样的命令查看了某一项本来压缩的数据时,需要把数据暂时解压,这时就设置recompress=1做一个标记,等有机会再把数据重新压缩。attempted_compress: 这个值只对Redis的自动化测试程序有用。我们不用管它。extra:其它扩展字段。目前Redis的实现里也没用上。quicklistNode中的zl指向一个ziplist,一个ziplist可以存储多个元素,结构如下图:
2.2 快速列表API
2.2.1 创建快速列表
/* Create a new quicklist.
* Free with quicklistRelease(). */
//创建快速列表,并对各个字段进行初始化
quicklist *quicklistCreate(void) {
struct quicklist *quicklist;
quicklist = zmalloc(sizeof(*quicklist));
quicklist->head = quicklist->tail = NULL;
quicklist->len = 0;
quicklist->count = 0;
quicklist->compress = 0;
quicklist->fill = -2;
quicklist->bookmark_count = 0;
return quicklist;
}
#define COMPRESS_MAX ((1 << QL_COMP_BITS)-1)
//设置压缩深度
void quicklistSetCompressDepth(quicklist *quicklist, int compress) {
if (compress > COMPRESS_MAX) {
compress = COMPRESS_MAX;
} else if (compress < 0) {
compress = 0;
}
quicklist->compress = compress;
}
#define FILL_MAX ((1 << (QL_FILL_BITS-1))-1)
//设置压缩列表的大小
void quicklistSetFill(quicklist *quicklist, int fill) {
if (fill > FILL_MAX) {
fill = FILL_MAX;
} else if (fill < -5) {
fill = -5;
}
quicklist->fill = fill;
}
//设置快速列表的参数
void quicklistSetOptions(quicklist *quicklist, int fill, int depth) {
quicklistSetFill(quicklist, fill);
quicklistSetCompressDepth(quicklist, depth);
}
/* Create a new quicklist with some default parameters. */
//通过一些默认参数创建快速列表
quicklist *quicklistNew(int fill, int compress) {
quicklist *quicklist = quicklistCreate();
quicklistSetOptions(quicklist, fill, compress);
return quicklist;
}
2.2.2 创建快速列表节点
REDIS_STATIC quicklistNode *quicklistCreateNode(void) {
quicklistNode *node;
node = zmalloc(sizeof(*node));
node->zl = NULL;
node->count = 0;
node->sz = 0;
node->next = node->prev = NULL;
//默认不压缩
node->encoding = QUICKLIST_NODE_ENCODING_RAW;
//默认使用压缩列表结构来存数据
node->container = QUICKLIST_NODE_CONTAINER_ZIPLIST;
node->recompress = 0;
return node;
}
2.2.3 添加元素
快速列表添加元素可以在 头部添加 quicklistPushHead 或 尾部添加 quicklistPushTail ,这两个函数的源码极其相似,这里只介绍头部添加:
/* Add new entry to head node of quicklist.
*
* Returns 0 if used existing head.
* Returns 1 if new head created. */
int quicklistPushHead(quicklist *quicklist, void *value, size_t sz) {
//获取快速列表的头节点
quicklistNode *orig_head = quicklist->head;
//判断头节点的空间是否足够容纳要添加的元素
if (likely(
_quicklistNodeAllowInsert(quicklist->head, quicklist->fill, sz))) {
//新增元素将会被编码为一个压缩列表节点,然后添加到快速列表的头节点对应的压缩列表中
quicklist->head->zl =
ziplistPush(quicklist->head->zl, value, sz, ZIPLIST_HEAD);
//更新快速列表头节点的 sz 字段
quicklistNodeUpdateSz(quicklist->head);
} else {
//头节点无法容纳下新增元素,则创建一个新的快速列表节点
quicklistNode *node = quicklistCreateNode();
//新增元素添加进这个新的快速列表节点里
node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_HEAD);
//更新这个快速列表节点的 sz 字段
quicklistNodeUpdateSz(node);
//将这个快速列表节点添加到头节点的前面
//这个就是往一个双向链表中添加节点
_quicklistInsertNodeBefore(quicklist, quicklist->head, node);
}
//总的压缩列表的节点数量增加
quicklist->count++;
//头节点对应的压缩列表节点数量增加
quicklist->head->count++;
return (orig_head != quicklist->head);
}
也可以在任意指定的位置添加元素,quicklistInsertBefore 和 quicklistInsertAfter 就是分别在指定位置前面和后面添加元素,以下为 Redis 中的测试代码,可以帮助我们了解其具体使用:
typedef struct quicklistEntry {
//被查找的快速列表
const quicklist *quicklist;
//查找到的元素所在的快速列表节点
quicklistNode *node;
//查找到的元素所在的压缩列表节点
unsigned char *zi;
//查找到的元素如果是字符串,则存在value字段
unsigned char *value;
//查找到的元素如果是整数,则存在longval字段
long long longval;
//查找到的元素长度
unsigned int sz;
//查找到的元素距离压缩列表头部/尾部隔了多少个节点
int offset;
} quicklistEntry;
TEST("insert after with 0 elements") {
//创建一个快速列表
quicklist *ql = quicklistNew(-2, options[_i]);
//quicklistEntry这个结构是辅组后面添加元素用的
quicklistEntry entry;
//这个函数用于找到添加位置的信息,并记录在entry中
//这里的第二个参数就是添加的位置,可以是正也可以是负
//为正时表示从快速列表头部开始往尾部方向多少个压缩列表节点的位置
//为负时表示从快速列表尾部开始往头部方向多少个压缩列表节点的位置
quicklistIndex(ql, 0, &entry);
//元素"abc"添加到entry指定位置的后面
quicklistInsertAfter(ql, &entry, "abc", 4);
ql_verify(ql, 1, 1, 1, 1);
quicklistRelease(ql);
}
在指定位置处添加元素比较复杂,也最常用,这里重点介绍一下:
void quicklistInsertBefore(quicklist *quicklist, quicklistEntry *entry,
void *value, const size_t sz) {
_quicklistInsert(quicklist, entry, value, sz, 0);
}
void quicklistInsertAfter(quicklist *quicklist, quicklistEntry *entry,
void *value, const size_t sz) {
_quicklistInsert(quicklist, entry, value, sz, 1);
}
/* Insert a new entry before or after existing entry 'entry'.
*
* If after==1, the new value is inserted after 'entry', otherwise
* the new value is inserted before 'entry'. */
REDIS_STATIC void _quicklistInsert(quicklist *quicklist, quicklistEntry *entry,
void *value, const size_t sz, int after) {
int full = 0, at_tail = 0, at_head = 0, full_next = 0, full_prev = 0;
int fill = quicklist->fill;
//获取插入位置的快速列表节点信息
quicklistNode *node = entry->node;
quicklistNode *new_node = NULL;
if (!node) {
//只有当快速列表的头节点或者尾节点不存在时才会走到这里
/* we have no reference node, so let's create only node in the list */
D("No node given!");
new_node = quicklistCreateNode();
//将添加的元素插入快速列表节点对应的压缩列表的节点头部
new_node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_HEAD);
__quicklistInsertNode(quicklist, NULL, new_node, after);
new_node->count++;
quicklist->count++;
return;
}
/* Populate accounting flags for easier boolean checks later */
//判断node节点对应的压缩列表是否能够容纳得下要添加的元素,即node节点是否已满
if (!_quicklistNodeAllowInsert(node, fill, sz)) {
D("Current node is full with count %d with requested fill %lu",
node->count, fill);
//当前节点已满
full = 1;
}
//如果添加元素的位置是在某个快速节点对应的压缩列表的尾部
//则要判断下一个快速列表节点对应的压缩列表是否能够容纳得下要添加得元素
if (after && (entry->offset == node->count)) {
D("At Tail of current ziplist");
//在尾部添加
at_tail = 1;
if (!_quicklistNodeAllowInsert(node->next, fill, sz)) {
D("Next node is full too.");
//下一个节点已满
full_next = 1;
}
}
//判断是否是在头部添加
if (!after && (entry->offset == 0)) {
D("At Head");
//在头部添加
at_head = 1;
if (!_quicklistNodeAllowInsert(node->prev, fill, sz)) {
D("Prev node is full too.");
//前一个节点已满
full_prev = 1;
}
}
/* Now determine where and how to insert the new element */
if (!full && after) {
D("Not full, inserting after current position.");
//当前节点解压缩
quicklistDecompressNodeForUse(node);
//在entry->zi这个压缩列表节点之和添加,所以插入位置为其后一个节点的起始位置
unsigned char *next = ziplistNext(node->zl, entry->zi);
if (next == NULL) {
node->zl = ziplistPush(node->zl, value, sz, ZIPLIST_TAIL);
} else {
node->zl = ziplistInsert(node->zl, next, value, sz);
}
node->count++;
quicklistNodeUpdateSz(node);
//添加完元素后再对压缩列表进行压缩
quicklistRecompressOnly(quicklist, node);
} else if (!full && !after) {
D("Not full, inserting before current position.");
quicklistDecompressNodeForUse(node);
//和上一个 if 的区别是在头部添加,所以插入位置就是entry->zi
node->zl = ziplistInsert(node->zl, entry->zi, value, sz);
node->count++;
quicklistNodeUpdateSz(node);
quicklistRecompressOnly(quicklist, node);
} else if (full && at_tail && node->next && !full_next && after) {
/* If we are: at tail, next has free space, and inserting after:
* - insert entry at head of next node. */
//当前节点已满,要添加在尾部,并且后一个节点存在,后一个节点也没用满
D("Full and tail, but next isn't full; inserting next node head");
new_node = node->next;
quicklistDecompressNodeForUse(new_node);
//新增元素添加到后一个节点对应的压缩列表的第一个节点前面
new_node->zl = ziplistPush(new_node->zl, value, sz, ZIPLIST_HEAD);
new_node->count++;
quicklistNodeUpdateSz(new_node);
quicklistRecompressOnly(quicklist, new_node);
} else if (full && at_head && node->prev && !full_prev && !after) {
/* If we are: at head, previous has free space, and inserting before:
* - insert entry at tail of previous node. */
//当前节点满了,要添加在头部,并且前一个节点存在,前一个节点没用满
D("Full and head, but prev isn't full, inserting prev node tail");
new_node = node->prev;
quicklistDecompressNodeForUse(new_node);
//新增元素添加到前一个节点对应的压缩列表的尾部
new_node->zl = ziplistPush(new_node->zl, value, sz, ZIPLIST_TAIL);
new_node->count++;
quicklistNodeUpdateSz(new_node);
quicklistRecompressOnly(quicklist, new_node);
} else if (full && ((at_tail && node->next && full_next && after) ||
(at_head && node->prev && full_prev && !after))) {
/* If we are: full, and our prev/next is full, then:
* - create new node and attach to quicklist */
//当前节点满了,插入位置在两端,并且插入方向的下一个节点也满了
D("\tprovisioning new node...");
//则创建一个新的快速列表节点存放要添加的元素
new_node = quicklistCreateNode();
new_node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_HEAD);
new_node->count++;
quicklistNodeUpdateSz(new_node);
__quicklistInsertNode(quicklist, node, new_node, after);
} else if (full) {
/* else, node is full we need to split it. */
/* covers both after and !after cases */
D("\tsplitting node...");
//当前节点满了,但是插入位置不在两端
quicklistDecompressNodeForUse(node);
//则要从插入位置把当前节点分裂成两个节点
new_node = _quicklistSplitNode(node, entry->offset, after);
new_node->zl = ziplistPush(new_node->zl, value, sz,
after ? ZIPLIST_HEAD : ZIPLIST_TAIL);
new_node->count++;
quicklistNodeUpdateSz(new_node);
__quicklistInsertNode(quicklist, node, new_node, after);
_quicklistMergeNodes(quicklist, node);
}
quicklist->count++;
}
2.2.4 删除元素
快速列表删除元素有两个函数 quicklistDelEntry 和 quicklistDelRange,分别是删除单个元素和删除某个区间的元素,其源码如下:
/* Delete one entry from list given the node for the entry and a pointer
* to the entry in the node.
*
* Note: quicklistDelIndex() *requires* uncompressed nodes because you
* already had to get *p from an uncompressed node somewhere.
*
* Returns 1 if the entire node was deleted, 0 if node still exists.
* Also updates in/out param 'p' with the next offset in the ziplist. */
REDIS_STATIC int quicklistDelIndex(quicklist *quicklist, quicklistNode *node,
unsigned char **p) {
int gone = 0;
//删除 node 节点对应的压缩列表 p 位置的元素
node->zl = ziplistDelete(node->zl, p);
//压缩列表节点数量减少
node->count--;
if (node->count == 0) {
gone = 1;
//当前快速节点对应的压缩列表没有元素,则删除当前快读列表节点,其操作同普通的双向链表删除节点
__quicklistDelNode(quicklist, node);
} else {
//更新当前快速列表节点 sz 字段的值
quicklistNodeUpdateSz(node);
}
//快速列表总的压缩列表节点数减少
quicklist->count--;
/* If we deleted the node, the original node is no longer valid */
return gone ? 1 : 0;
}
/* Delete one element represented by 'entry'
*
* 'entry' stores enough metadata to delete the proper position in
* the correct ziplist in the correct quicklist node. */
void quicklistDelEntry(quicklistIter *iter, quicklistEntry *entry) {
quicklistNode *prev = entry->node->prev;
quicklistNode *next = entry->node->next;
//删除元素,返回值 deleted_node 表示当前节点是否要删除
int deleted_node = quicklistDelIndex((quicklist *)entry->quicklist,
entry->node, &entry->zi);
/* after delete, the zi is now invalid for any future usage. */
iter->zi = NULL;
/* If current node is deleted, we must update iterator node and offset. */
if (deleted_node) {
if (iter->direction == AL_START_HEAD) {
iter->current = next;
iter->offset = 0;
} else if (iter->direction == AL_START_TAIL) {
iter->current = prev;
iter->offset = -1;
}
}
/* else if (!deleted_node), no changes needed.
* we already reset iter->zi above, and the existing iter->offset
* doesn't move again because:
* - [1, 2, 3] => delete offset 1 => [1, 3]: next element still offset 1
* - [1, 2, 3] => delete offset 0 => [2, 3]: next element still offset 0
* if we deleted the last element at offet N and now
* length of this ziplist is N-1, the next call into
* quicklistNext() will jump to the next node. */
}
/* Delete a range of elements from the quicklist.
*
* elements may span across multiple quicklistNodes, so we
* have to be careful about tracking where we start and end.
*
* Returns 1 if entries were deleted, 0 if nothing was deleted. */
int quicklistDelRange(quicklist *quicklist, const long start,
const long count) {
if (count <= 0)
return 0;
//要删除的元素数量是包含 start 位置的元素
unsigned long extent = count; /* range is inclusive of start position */
//不管是从头节点往尾节点方向删除还是反方向删除,如果要删除的元素不够,则要删除的元素数量
//是从删除位置沿着删除方向剩余的元素数量
if (start >= 0 && extent > (quicklist->count - start)) {
/* if requesting delete more elements than exist, limit to list size. */
extent = quicklist->count - start;
} else if (start < 0 && extent > (unsigned long)(-start)) {
/* else, if at negative offset, limit max size to rest of list. */
extent = -start; /* c.f. LREM -29 29; just delete until end. */
}
quicklistEntry entry;
//找到要删除的元素,并将元素相关的一系列信息保存到entry中
if (!quicklistIndex(quicklist, start, &entry))
return 0;
D("Quicklist delete request for start %ld, count %ld, extent: %ld", start,
count, extent);
quicklistNode *node = entry.node;
/* iterate over next nodes until everything is deleted. */
while (extent) {
quicklistNode *next = node->next;
unsigned long del;
int delete_entire_node = 0;
if (entry.offset == 0 && extent >= node->count) {
//如果开始位置在压缩列表的头部,并且要删除的元素多余该压缩列表的总元素数量
//则删除当前的压缩列表
/* If we are deleting more than the count of this node, we
* can just delete the entire node without ziplist math. */
delete_entire_node = 1;
del = node->count;
} else if (entry.offset >= 0 && extent >= node->count) {
/* If deleting more nodes after this one, calculate delete based
* on size of current node. */
//个人觉得这里存在问题
//应该是 entry.offset >= 0 && extent >= (node->count - entry.offset)
//并且entry.offset肯定不等于0,不然程序就走进第一个if了,不会走到这里
del = node->count - entry.offset;
} else if (entry.offset < 0) {
/* If offset is negative, we are in the first run of this loop
* and we are deleting the entire range
* from this start offset to end of list. Since the Negative
* offset is the number of elements until the tail of the list,
* just use it directly as the deletion count. */
//从尾部向头部删除
del = -entry.offset;
/* If the positive offset is greater than the remaining extent,
* we only delete the remaining extent, not the entire offset.
*/
if (del > extent)
del = extent;
} else {
/* else, we are deleting less than the extent of this node, so
* use extent directly. */
del = extent;
}
D("[%ld]: asking to del: %ld because offset: %d; (ENTIRE NODE: %d), "
"node count: %u",
extent, del, entry.offset, delete_entire_node, node->count);
if (delete_entire_node) {
//删除当前的快速列表节点
__quicklistDelNode(quicklist, node);
} else {
//解压缩
quicklistDecompressNodeForUse(node);
//删除对应的压缩列表上的这个范围的节点
node->zl = ziplistDeleteRange(node->zl, entry.offset, del);
quicklistNodeUpdateSz(node);
node->count -= del;
quicklist->count -= del;
quicklistDeleteIfEmpty(quicklist, node);
if (node)
quicklistRecompressOnly(quicklist, node);
}
//计算剩余要删除的元素数量
extent -= del;
//还有元素要删,转到下一个节点
node = next;
//从下一个节点的头部/尾部开始删除
entry.offset = 0;
}
return 1;
}
2.2.5 查找元素
Redis 中是无法直接查找到某个元素的,只能从某个位置开始朝着一个方向遍历,一个元素一个元素地比较是否是要找的元素,在 Redis 中有如下测试代码:
quicklistIter *iter = quicklistGetIterator(ql, AL_START_HEAD);
quicklistEntry entry;
int i = 0;
while (quicklistNext(iter, &entry)) { //获取迭代器中的下一个元素
//比较当前的压缩列表节点存储的元素与所要查找的是否相同
if (quicklistCompare(entry.zi, (unsigned char *)"bar", 3)) {
quicklistDelEntry(iter, &entry);
}
i++;
}
quickllist 为了遍历节点,提供了自己的迭代器:
/* Directions for iterators */
#define AL_START_HEAD 0
#define AL_START_TAIL 1
typedef struct quicklistIter {
//当前操作的 quicklist
const quicklist *quicklist;
//当前所指向的快速列表节点
quicklistNode *current;
//当前迭代的ziplist节点位置,初始化时该指针为NULL
unsigned char *zi;
//当前操作的压缩列表节点的偏移量
long offset; /* offset in current ziplist */
//迭代方向,0:从头部往尾部,1:反之
int direction;
} quicklistIter;