数据库
首页 > 数据库> > Redis5设计与源码分析 (第8章 Stream)

Redis5设计与源码分析 (第8章 Stream)

作者:互联网

Redis在最新的5.0.0版本中也加入了消息队列的功能,这就是Stream。

8.1 Stream简介

图8-1 Redis Stream结构图

命令: xadd mystream1 * name hb age 20

mystream1为Stream的名称;

*代表由Redis自行生成消息ID;

name、age为该消息的field;

hb、20则为对应的field的值。

每个消息都由以下两部分组成。

·每个消息有唯一的消息ID,消息ID严格递增。

·消息内容由多个field-value对组成。

 

当消费者不归属于任何消费组时,该消费者可以消费消息队列中的任何消息。

 

消费组特点。

·每个消费组通过组名称唯一标识,每个消费组都可以消费该消息队列的全部消息,多个消费组之间相互独立。

·每个消费组可以有多个消费者,消费者通过名称唯一标识,消费者之间的关系是竞争关系,也就是说一个消息只能由该组的一个成员消费。

·组内成员消费消息后需要确认,每个消息组都有一个待确认消息队列(pending entry list,pel),用以维护该消费组已经消费但没有确认的消息。

·消费组中的每个成员也有一个待确认消息队列,维护着该消费者已经消费尚未确认的消息。

RedisStream的底层实现主要使用了listpack以及Rax树。

8.1.1 Stream底层结构listpack

Redis listpack可用于存储字符串或者整型。图8-2为listpack的整体结构图。

图8-2 listpack结构图

listpack由4部分组成:Total Bytes、Num Elem、Entry以及End,

下面介绍各部分的具体含义。

1)Total Bytes为整个listpack的空间大小,占用4个字节,每个listpack最多占用4294967295Bytes。

2)Num Elem为listpack中的元素个数,即Entry的个数,占用2个字节,值得注意的是,这并不意味着listpack最多只能存放65535个Entry,当Entry个数大于等于65535时,Num Elem被设置为65535,此时如果需要获取元素个数,需要遍历整个listpack。

3)End为listpack结束标志,占用1个字节,内容为0xFF。

4)Entry为每个具体的元素。其内容可以为字符串或者整型,每个Entry由3部分组成,每部分的具体含义如下。Encode为该元素的编码方式,占用1个字节,之后是内容字段content,二者紧密相连。表8-1详细介绍了Encode字段。

表8-1 listpack Encode

backlen记录了这个Entry的长度(Encode+content),注意并不包括backlen自身的长度,占用的字节数小于等于5。backlen所占用的每个字节的第一个bit用于标识;0代表结束,1代表尚未结束,每个字节只有7 bit有效。值得一提的是,backlen主要用于从后向前遍历,当我们需要找到当前元素的上一个元素时,我们可以从后向前依次查找每个字节,找到上一个Entry的backlen字段的结束标识,进而可以计算出上一个元素的长度。例如backlen为0000000110001000,代表该元素的长度为00000010001000,即136字节。通过计算即可算出上一个元素的首地址(entry的首地址)。值得注意的是,在整型存储中,并不实际存储负数,而是将负数转换为正数进行存储。例如,在13位整型存储中,存储范围为[0,8191],其中[0,4095]对应非负的[0,4095](当然,[0,127]将会采用7位无符号整型存储),而[4096,8191]则对应[-4096,-1]。

 

8.1.2 Stream底层结构Rax简介

1.概要

前缀树是字符串查找时,经常使用的一种数据结构,能够在一个字符串集合中快速查找到某个字符串,下面给出一个简单示例,如图8-3所示。

图8-3 前缀树示例                 图8-4 只有一个压缩节点的Rax

Rax树通过节点压缩节省空间,只有一个key(foo)的Rax树如图8-4所示,其中中括号代表非压缩节点,双引号代表压缩节点(压缩节点,非压缩节点下文将详细介绍),(iskey=1)代表该节点存储了一个key,

在上述节点的基础上插入key(foobar)后,Rax树结构如图8-5所示。

图8-5 包含两个压缩节点的Rax

含有两个key(foobar,footer)的Rax树结构图如图8-6所示。

图8-6 含有foobar、footer两个key的Rax

对于非压缩节点,其内部字符是按照字典序排序的,例如上述第二个节点,含有2个字符b、t,二者是按照字典序排列的。

2.关键结构体介绍

1)rax结构代表一个Rax树,它包含3个字段,指向头节点的指针,元素个数(即key的个数)以及节点个数。

typedef struct rax {

raxNode *head;

uint64_t numele;

uint64_t numnodes;

} rax;

 

2)raxNode代表Rax树中的一个节点,它的定义如下:

typedef struct raxNode {

uint32_t iskey:1; /*当前节点是否包含一个key,占用1bit*/

uint32_t isnull:1; /* 当前key对应的value是否为空,占用1bit */

uint32_t iscompr:1; /* 当前节点是否为压缩节点,占用1bit*/

uint32_t size:29; /* 为压缩节点压缩的字符串长度或者非压缩节点的子节点个

数,占用29bit; */

unsigned char data[]; // 中包含填充字段,同时存储了当前节点包含的字符串以及子

节点的指针、key对应的value指针。

} raxNode;

 

raxNode分为2类,压缩节点和非压缩节点,下面分别进行介绍。

1)压缩节点 。我们假设该节点存储的内容为字符串ABC,其结构图如图8-7所示。

图8-7 压缩节点示例图

·iskey为1且isnull为0时,value-ptr存在,否则value-ptr不存在;

·iscompr为1代表当前节点是压缩节点,size为3代表存储了3个字符;

·紧随size的是该节点存储的字符串,根据字符串的长度确定是否需要填充字段(填充必要的字节,使得后面的指针地址放到合适的位置上);

·由于是压缩字段,故而只有最后一个字符有子节点。(c-ptr)

2)非压缩节点 。我们假设其内容为XY,结构图如图8-8所示。

图8-8 非压缩节点示例图

与压缩节点的不同点在于,每个字符都有一个子节点,值得一提的是,字符个数小于2时,都是非压缩节点。为了实现Rax树的遍历,Redis提供了raxStack及raxIterator两种结构,下面逐一介绍。

①raxStack结构用于存储从根节点到当前节点的路径,具体定义如下:

#define RAX_STACK_STATIC_ITEMS 32

typedef struct raxStack {

void **stack; /*用于记录路径,该指针可能指向static_items(路径较短时)或者堆空间内存; */

size_t items, maxitems; /* 代表stack指向的空间的已用空间以及最大空间 */

void *static_items[RAX_STACK_STATIC_ITEMS];

int oom; /* 代表当前栈是否出现过内存溢出. */

} raxStack;

 

②raxIterator用于遍历Rax树中所有的key,该结构的定义如下:

typedef struct raxIterator {

int flags; //当前迭代器标志位,目前有3种,

RAX_ITER_JUST_SEEKED代表当前迭代器指向的元素是刚刚搜索过的,当需要从迭代器中获取元素时,直接返回当前元素并清空该标志位即可;

RAX_ITER_EOF代表当前迭代器已经遍历到rax树的最后一个节点;

RAX_ITER_SAFE代表当前迭代器为安全迭代器,可以进行写操作。

rax *rt; /* 当前迭代器对应的rax */

unsigned char *key; /*存储了当前迭代器遍历到的key,该指针指向

key_static_string或者从堆中申请的内存。*/

void *data; /* 当前key关联的value值 */

size_t key_len; /* key指向的空间的已用空间 */

size_t key_max; /*key 最大空间 */

unsigned char key_static_string[RAX_ITER_STATIC_LEN]; //默认存储空间,当key比较大时,会使用堆空间内存。

raxNode *node; /* 当前key所在的raxNode */

raxStack stack; /* 记录了从根节点到当前节点的路径,用于raxNode的向上遍历。*/

raxNodeCallback node_cb; /* 为节点的回调函数,通常为空*/

} raxIterator;

 

 

8.1.3 Stream结构

图8-9 Stream结构示例

每个消息流都包含一个Rax结构。以消息ID为key、listpack结构为value存储在Rax结构中。每个消息的具体信息存储在这个listpack中。以下亮点是值得注意的。

1)每个listpack都有一个master entry,该结构中存储了创建这个listpack时待插入消息的所有field,这主要是考虑同一个消息流,消息内容通常具有相似性,如果后续消息的field与master entry内容相同,则不需要再存储其field。

2)每个listpack中可能存储多条消息。

  1. 消息存储

(1)消息ID

streamID定义如下,以每个消息创建时的时间(1970年1月1号至今的毫秒数)以及序号组成,共128位。

typedef struct streamID {

uint64_t ms; /* Unix time in milliseconds. */

uint64_t seq; /* Sequence number. */

} streamID;

 

(2)消息存储的格式

Stream的消息内容存储在listpack中, listpack用于存储字符串或者整型数据,listpack中的单个元素称为entry,下文介绍的消息存储格式的每个字段都是一个entry,并不是将整个消息作为字符串储存的。值得注意的是,每个listpack会存储多个消息,具体存储的消息个数是由stream-node-max-bytes(listpack节点最大占用的内存数,默认4096)和stream-node-max-entries(每个listpack最大存储的元素个数,默认100)决定的。

·每个消息会占用多个listpack entry。

·每个listpack会存储多个消息。

每个listpack在创建时,会构造该节点的master entry(根据第一个插入的消息构建),其结构如图8-10所示。

图8-10 listpack master entry结构

·count 为当前listpack中的所有未删除的消息个数。

·deleted 为当前listpack中所有已经删除的消息个数。

·num-fields 为下面的field的个数。

·field-1,…,filed-N 为当前listpack中第一个插入的消息的所有field

域。

·0 为标识位,在从后向前遍历该listpack的所有消息时使用。

处省略了listpack每个元素存储时的encoding以及backlen字段;

 

消息的field域与master entry的域完全相同

存储一个消息时,如果该消息的field域与master entry的域完全相同,则不需要再次存储field域,此时其消息存储如图8-11所示。

图8-11 消息存储

 

·flags字段为消息标志位,STREAM_ITEM_FLAG_NONE代表无特殊标识, STREAM_ITEM_FLAG_DELETED代表该消息已经被删除, STREAM_ITEM_FLAG_SAMEFIELDS代表该消息的field域与master entry完全相同。

·streamID.ms以及streamID.seq为该消息ID减去master entry id之后的值。

·value域存储了该消息的每个field域对应的内容。

·lp-count为该消息占用listpack的元素个数,也就是3+N。

 

消息的field域与master entry不完全相同

如果该消息的field域与master entry不完全相同,此时消息的存储如图8-12所示。

·flags为消息标志位,与上面一致;

·streamID.ms,streamID.seq为该消息ID减去master entry id之后的值;

·num-fields为该消息field域的个数;

·field-value存储了消息的域值对,也就是消息的具体内容;

·lp-count为该消息占用的listpack的元素个数,也就是4+2N。

图8-12 消息存储

 

2.关键结构体介绍

  1. stream。

typedef struct stream {

rax *rax; /* 存储生产者生产的具体消息,以消息ID为键,消息内容为值存储在rax中,值得注意的是,rax中的一个节点可能存储多个消息*/

uint64_t length; /*当前stream中的消息个数(不包括已经删除的消息)。*/

streamID last_id; /* 当前stream中最后插入的消息的ID,stream空时,设置为0。. */

rax *cgroups; /* 存储了当前stream相关的消费组,rax中: name -> streamCG */

} stream;

 

  1. 消费组。

每个Stream会有多个消费组,每个消费组通过组名称进行唯一标识,同时关联一个streamCG结构,该结构定义如下:

typedef struct streamCG {

streamID last_id; // 该消费组已经确认的最后一个消息的ID

rax *pel; // 该消费组尚未确认的消息,消息ID为键,streamNACK(一个尚未确认的消息)为值;

rax *consumers; // 该消费组中所有的消费者,消费者的名称为键,streamConsumer(代表一个消费者)为值。

} streamCG;

 

  1. 消费者。

每个消费者通过streamConsumer唯一标识,该结构如下:

typedef struct streamConsumer {

mstime_t seen_time; /* 该消费者最后一次活跃的时间; */

sds name; /* C消费者的名称*/

rax *pel; /* 消费者尚未确认的消息,以消息ID为键,streamNACK为值。 */

} streamConsumer;

 

  1. 未确认消息。

未确认消息(streamNACK)维护了消费组或者消费者尚未确认的消息,值得注意的是,消费组中的pel的元素与每个消费者的pel中的元素是共享的,即该消费组消费了某个消息,这个消息会同时放到消费组以及该消费者的pel队列中,并且二者是同一个streamNACK结构。

/* Pending (yet not acknowledged) message in a consumer group. */

typedef struct streamNACK {

mstime_t delivery_time; /* 该消息最后发送给消费方的时间 */

uint64_t delivery_count; /*为该消息已经发送的次数(组内的成员可以通过xclaim命令获取某个消息的处理权,该消息已经分给组内另一个消费者但其并没有确认该消息)。*/

streamConsumer *consumer; /* 该消息当前归属的消费者 */

} streamNACK;

 

5)迭代器。为了遍历stream中的消息:

typedef struct streamIterator {

stream *stream; /*当前迭代器正在遍历的消息流 */

streamID master_id; /* 消息内容实际存储在listpack中,每个listpack都有一个masterentry(也就是第一个插入的消息),master_id为该消息id */

uint64_t master_fields_count; /* master entry中field域的个数. */

unsigned char *master_fields_start; /*master entry field域存储的首地址*/

unsigned char *master_fields_ptr; /*当listpack中消息的field域与master entry的field域完全相同时,该消息会复用master entry的field域,在我们遍历该消息时,需要记录

当前所在的field域的具体位置,master_fields_ptr就是实现这个功能的。 */

int entry_flags; /* 当前遍历的消息的标志位 */

int rev; /*当前迭代器的方向 */

uint64_t start_key[2]; /* 该迭代器处理的消息ID的范围 */

uint64_t end_key[2]; /* End key as 128 bit big endian. */

raxIterator ri; /*rax迭代器,用于遍历rax中所有的key. */

unsigned char *lp; /* 当前listpack指针*/

unsigned char *lp_ele; /* 当前正在遍历的listpack中的元素, cursor. */

unsigned char *lp_flags; /* Current entry flags pointer.指向当前消息的flag域 */

//用于从listpack读取数据时的缓存

unsigned char field_buf[LP_INTBUF_SIZE];

unsigned char value_buf[LP_INTBUF_SIZE];

} streamIterator;

 

8.2 Stream底层结构listpack的实现

结构查询效率低,并且只适合于末尾增删。考虑到消息流中,通常只需要向其末尾增加消息,故而可以采用该结构;

8.2.1 初始化

图8-13 listpack初始化

/* Create a new, empty listpack.

* On success the new listpack is returned, otherwise an error is returned. */

unsigned char *lpNew(void) {

// LP_HDR_SIZE = 6,为listpack的头部

unsigned char *lp = lp_malloc(LP_HDR_SIZE+1); // 申请空间

if (lp == NULL) return NULL;

lpSetTotalBytes(lp,LP_HDR_SIZE+1);

lpSetNumElements(lp,0);

lp[LP_HDR_SIZE] = LP_EOF; // LP_EOF = 0xFF

return lp;

}

 

8.2.2 增删改操作

listpack提供了2种添加元素的方式:

一种是在任意位置插入元素,一种是在末尾插入元素。在末尾插入元素的底层实现通过调用任意位置插入元素进行,具体实现为lpInsert函数。

listpack的删除操作被转换为用空元素替换的操作。

listpack的替换操作(即改操作)的底层实现也是通过lpInsrt函数实现的。

 

lpInsert 函数定义:

unsigned char *lpInsert(unsigned char *lp, unsigned char *ele, uint32_t size, unsigned char *p, int where, unsigned char **newp);

·lp 为当前待操作的listpack;

·ele 为待插入的新元素或者待替换的新元素,ele为空时,也就是删除操作;

·size 为ele的长度;

·p 为待插入的位置或者带替换的元素位置;

·where 有LP_BEFORE(前插)、LP_AFTER(后插)、LP_REPLACE(替换);

·*newp 用于返回插入的元素、替换的元素、删除元素的下一个元素。

该函数返回null或者插入的元素,替换的元素,删除元素的下一个元素。

 

删除或者替换的主要过程如下:

1)计算需要插入的新元素或者替换旧元素的新元素需要的空间;

2)计算进行插入或者替换后整个listpack所需的空间,通过realloc申请空间;

3)调整新的listpack中的老的元素的位置,为待操作元素预留空间;

4)释放旧的listpack;

5)在新的listpack中进行插入或替换的操作;

6)更新新的listpack结构头部的统计信息。

 

8.2.3 遍历操作

核心思想是利用每个entry的encode或者backlen字段获取当前entry的长度;

unsigned char *lpFirst(unsigned char *lp); //获取第一个元素位置

unsigned char *lpLast(unsigned char *lp); //获取最后一个元素位置

unsigned char *lpNext(unsigned char *lp, unsigned char *p); //下一个元素位置

unsigned char *lpPrev(unsigned char *lp, unsigned char *p); //上一个元素位置

例如:

unsigned char *lpFirst(unsigned char *lp) {

lp += LP_HDR_SIZE; /* Skip the header. */ LP=+6

if (lp[0] == LP_EOF) return NULL; //0xFF

return lp;

}

此处获取的仅仅是某个entry首地址的指针,如果要读取当前元素则需要使用下 lpGet接口;

 

8.2.4 读取元素

lpGet用于获取p指向的Listpack中真正存储的元素:

①当元素采用字符串编码时,返回字符串的第一个元素位置,count为元素个数;

②当采用整型编码时,若intbuf不为空,则将整型数据转换为字符串存储在intbuf中,count为元素个数,并返回intbuf。若intbuf为空,直接将数据存储在count中,返回null。

unsigned char *lpGet(unsigned char *p, int64_t *count, unsigned char *intbuf)

lpGet的实现较为简单,主要是利用了每个entryencode字段(p[0];

 

8.3 Stream底层结构Rax的实现

 

Stream的消息内容存储在listpack中,但是如果将所有消息都存储在一个listpack中,则会存在效率问题。例如,查询某个消息时,需要遍历整个listpack;插入消息时,需要重新申请一块很大的空间。为了解决这些问题,Redis Stream通过Rax组织这些listpack ;

 

8.3.1 初始化

/* 分配一个新的rax并返回其指针。在内存不足时,函数*返回NULL. */

rax *raxNew(void) {

rax *rax = rax_malloc(sizeof(*rax)); //申请空间

if (rax == NULL) return NULL;

rax->numele = 0; //当前元素个数为0

rax->numnodes = 1; //当前节点个数为1

rax->head = raxNewNode(0,0); //构造头节点

if (rax->head == NULL) {

rax_free(rax);

return NULL;

} else {

return rax;

}

}

图8-14 Rax初始化

8.3.2 查找元素

/* 获取key对应的value值, */

//在rax中查找长度为len的字符串s(s为rax中的一个key), 找到返回该key对应的value

void *raxFind(rax *rax, unsigned char *s, size_t len) {

raxNode *h;

int splitpos = 0;

size_t i = raxLowWalk(rax,s,len,&h,NULL,&splitpos,NULL);

if (i != len || (h->iscompr && splitpos != 0) || !h->iskey)

return raxNotFound; //没有找到这个key

return raxGetData(h); //查到key, 将key对应的value返回

}

 

raxLowWalk为查找key的核心函数 ;

static inline size_t raxLowWalk(rax *rax, unsigned char *s, size_t len, raxNode **stopnode, raxNode ***plink, int *splitpos, raxStack *ts) ;

·rax 为待查找的Rax;

·s 为待查找的key;

·len 为s的长度;

·*stopnode 为 找过程中的终止节点,也就意味着,当rax查找到该节点时,待查找的key已经匹配完成,或者当前节点无法与带查找的key匹配;

·*plink 用于记录父节点中指向*stopnode的指针的位置,当*stopnode变化时,也需要修改父节点指向该节点的指针;

·*splitpos 用于记录压缩节点的匹配位置;

·当ts 不为空时,会将查找该key的路径写入该变量。

该函数返回s的匹配长度,当s!=len时,表示未查找到该key;当s==len时,需要检验*stopnode是否为key,并且当*stopnode为压缩节点时,还需要检查splitpos是否为0(可能匹配到某个压缩节点中间的某个元素)。

 

raxLowWalk函数的执行过程可以分为如下几步。

1)初始化变量。

2)从rax根节点开始查找,知道当前待查找节点无子节点或者s查找完毕。对于每个节点来说,如果为压缩节点,则需要与s中的字符完全匹配。如果为非压缩节点,则查找与当前待匹配字符相同的字符。

3)如果当前待匹配节点能够与s匹配,则移动位置到其子节点,继续匹配。

raxNode *h = rax->head; // 从根节点开始匹配

raxNode **parentlink = &rax->head;

size_t i = 0; /*当前待匹配字符位置. */

size_t j = 0; /* 当前匹配的节点的位置*/

while(h->size && i < len) { // 当前节点有子节点且尚未走到s字符串的末尾

unsigned char *v = h->data;

if (h->iscompr) { // 压缩节点是否能够完全匹配s字符串

for (j = 0; j < h->size && i < len; j++, i++) {

if (v[j] != s[i]) break;

}

if (j != h->size) break; // 当前压缩节点不能完全匹配或者s已经到达末尾

} else {

/* 非压缩节点遍历节点元素, 查找与当前字符匹配的位置*/

for (j = 0; j < h->size; j++) {

if (v[j] == s[i]) break;

}

if (j == h->size) break; // 未在非压缩节点找到匹配的字符

i++; // 非压缩节点可以匹配, 移动到s的下一个字符

}

// 当前节点能够匹配s

if (ts) raxStackPush(ts,h); /* Save stack of parent nodes. */

raxNode **children = raxNodeFirstChildPtr(h);

if (h->iscompr) j = 0; /* Compressed node only child is at index 0. */

memcpy(&h,children+j,sizeof(h)); // 将当前节点移动到其第 j个子节点

parentlink = children+j;

j = 0;

}

if (stopnode) *stopnode = h;

if (plink) *plink = parentlink;

if (splitpos && h->iscompr) *splitpos = j;

return i;

 

8.3.3 添加元素

对于已存在的key,rax提供了2种方案,覆盖或者不覆盖原有的value,对应的接口分别为raxInsert、raxTryInsert,两个接口的定义如下:

/* 覆盖插入*/

int raxInsert(rax *rax, unsigned char *s, size_t len, void *data, void **old) {

return raxGenericInsert(rax,s,len,data,old,1);

}

/*非覆盖插入函数:如果存在具有相同键的元素,则不更新值,并且返回0。 */

int raxTryInsert(rax *rax, unsigned char *s, size_t len, void *data, void **old) {

return raxGenericInsert(rax,s,len,data,old,0);

}

 

raxGenericInsert 函数

函数参数与raxInsert基本一致,只是增加overwrite用于标识key存在时是否覆盖;

1.查找key是否存在

size_t i;

int j = 0; /* 分割位置。如果raxLowWalk()在压缩节点中停止,则索引" j"表示我们在压缩节点中停止的字符,即拆分该节点以进行插入的位置 */

raxNode *h, **parentlink;

i = raxLowWalk(rax,s,len,&h,&parentlink,&j,NULL);

 

2.找到key

根据raxLowWalk的返回值,如果当前key已经存在,则直接对该节点进行操作 ;

if (i == len && (!h->iscompr || j == 0 /* not in the middle if j is 0 */)) {

/*查看之前是否存储value,没有则申请空间 . */

if (!h->iskey || (h->isnull && overwrite)) {

h = raxReallocForData(h,data);

if (h) memcpy(parentlink,&h,sizeof(h));

}

if (h == NULL) {

errno = ENOMEM;

return 0;

}

/* 更新存在的key. */

if (h->iskey) {

if (old) *old = raxGetData(h);

if (overwrite) raxSetData(h,data);

errno = 0;

return 0; /* Element already exists. */

}

/*否则,将节点设置为键 set h->iskey. */

raxSetData(h,data);

rax->numele++;

return 1; /* Element inserted. */

}

 

3.key不存在

1)在查找key的过程中,如果最后停留在某个压缩节点上,此时需要对该压缩节点进行拆分,具体拆分情况分为以下几种,以图8-15为例。

图8-15 Rax节点拆分

·插入key"ciao",需要将"annibale"节点拆分为2部分:非压缩节点,压缩节点。

·插入key"ago",需要将"annibale"节点拆分为3部分:非压缩节点,非压缩节点,压缩节点。

·插入key"annienter",需要将"annibale"节点拆分3部分:压缩节点,非压缩节点,压缩节点。

·插入key"annibaie",需要将"annibale"拆成3部分:压缩节点,非压缩节点,非压缩节点。

·插入key"annibali",需要将"annibale"拆成2部分:压缩节点,非压缩节点。

·插入key"a",将"annibale"拆分成2部分:非压缩节点,压缩节点。

·插入key"anni",将"annibale"拆分成2个压缩节点。

总体而言分为2类:

新插入的key是当前节点的一部分: 将压缩节点进行拆分后直接设置新的key-value即可

新插入的key和压缩节点的某个位置不匹配: 对 需要在拆分后的相应位置的非压缩节点中插入新key的相应不匹配字符,之后将新key的剩余部分插入到这个非压缩节点的子节点中。

 

2)如果查找key完成后,不匹配节点为某个非压缩节点,或者某个压缩节点的某个字符不匹配,进行节点拆分后导致的不匹配位置为拆分后创建的非压缩节点,此时仅仅需要将当前待匹配字符插入到这个非压缩节点上(注意字符按照字典序排列),并为其创建子节点。之后,将剩余字符放入新建的子节点中即可(如果字符长度过长,需要进行分割)。

 

8.3.4 删除元素

Rax的删除操作主要有3个接口,可以删除rax中的某个key,或者释放整个rax,在释放rax时,还可以设置释放回调函数,在释放rax的每个key时,都会调用这个回调函数;

// 在rax中删除长度为len的s(s代表待删除的key), *old用于返回该key对应的value

int raxRemove(rax *rax, unsigned char *s, size_t len, void **old);

// 释放rax

void raxFree(rax *rax);

// 释放rax,释放每个key时,都会调用free_callback函数

void raxFreeWithCallback(rax *rax, void (*free_callback)(void*));

rax的释放操作,采用的是深度优先算法;

 

raxRemove函数

当删除rax中的某个key-value对时,首先查找key是否存在,不存在则直接返回,存在则需要进行删除操作。

 

raxNode *h;

raxStack ts;

raxStackInit(&ts);

int splitpos = 0;

size_t i = raxLowWalk(rax,s,len,&h,NULL,&splitpos,&ts);

if (i != len || (h->iscompr && splitpos != 0) || !h->iskey) {

raxStackFree(&ts); // 没有找到需要删除的key

return 0;

}

 

如果key存在,则需要进行删除操作,删除操作完成后,Rax树可能需要进行压缩。具体可以分为下面2种情况,此处所说的压缩是指将某个节点与其子节点压缩成一个节点,叶子节点没有子节点,不能进行压缩。

1)某个节点只有一个子节点,该子节点之前是key,经删除操作后不再是key,此时可以将该节点与其子节点压缩,如图8-16所示,删除foo后,可以将Rax进行压缩,压缩后为"foobar"->[](iskey=1)。

图8-16 Rax节点压缩             图8-17 Rax节点压缩

2)某个节点有两个子节点,经过删除操作后,只剩下一个子节点,如果这个子节点不是key,则可以将该节点与这个子节点压缩。如图8-17所示,删除foobar后,可以将Rax树进行压缩,压缩成"footer"->[](iskey=1)。

 

删除操作具体可以分为2个阶段,删除阶段以及压缩阶段。例如,图8-17删除"foobar"时,需要从下向上,删除可以删除的节点。图8-16在删除"foo"时,则不需要删除节点。这部分的实现逻辑主要是利用查找key时记录的匹配路径,依次向上直到无法删除为止。

 

if (h->size == 0) {

raxNode *child = NULL;

while(h != rax->head) {

child = h;

rax_free(child);

rax->numnodes--;

h = raxStackPop(&ts);

/* 如果节点为key或者子节点个数不为1,则无法继续删除 */

if (h->iskey || (!h->iscompr && h->size != 1)) break;

}

if (child) {

raxNode *new = raxRemoveChild(h,child);

if (new != h) {

raxNode *parent = raxStackPeek(&ts);

raxNode **parentlink;

if (parent == NULL) {

parentlink = &rax->head;

} else {

parentlink = raxFindParentLink(parent,h);

}

memcpy(parentlink,&new,sizeof(new));

}

 

/* 删除后查看是否可以尝试压缩 node has just a single child and is not a key, */

if (new->size == 1 && new->iskey == 0) {

trycompress = 1;

h = new;

}

}

} else if (h->size == 1) {

/* 可以尝试进行压缩. */

trycompress = 1;

}

 

压缩过程可以细化为2步。

①找到可以进行压缩的第一个元素,之后将所有可进行压缩的节点进行压缩。由于raxRowWalk函数已经记录了查找key的过程,压缩时只需从记录栈中不断弹出元素,即可找到可进行压缩的第一个元素,过程如下:

raxNode *parent;

while(1) {

parent = raxStackPop(&ts);

if (!parent || parent->iskey ||

(!parent->iscompr && parent->size != 1)) break;

h = parent; //可以进行压缩

}

raxNode *start = h; /* 可以进行压缩的第一个节点. */

 

②找到第一个可压缩节点后,进行数据压缩。由于可压缩的节点都只有一个子节点,压缩过程只需要读取每个节点的内容,创建新的节点,并填充新节点的内容即可,此处省略。

 

8.3.5 遍历元素

Redis中实现的迭代器为双向迭代器,可以向前,也可以向后,顺序是按照key的字典序排列的。通过rax的结构图可以看出,如果某个节点为key,则其子节点的key按照字典序比该节点的key大。另外,如果当前节点为非压缩节点,则其最左侧节点的key是其所有子节点的key中最小的。迭代器的主要接口有:

void raxStart(raxIterator *it, rax *rt);

int raxSeek(raxIterator *it, const char *op, unsigned char *ele, size_t len);

int raxNext(raxIterator *it);

int raxPrev(raxIterator *it);

void raxStop(raxIterator *it);

int raxEOF(raxIterator *it);

1.raxStart

raxStart用于初始化raxIterator结构;

void raxStart(raxIterator *it, rax *rt) {

it->flags = RAX_ITER_EOF; /*默认值为迭代结束. */

it->rt = rt;

it->key_len = 0;

it->key = it->key_static_string;

it->key_max = RAX_ITER_STATIC_LEN;

it->data = NULL;

it->node_cb = NULL;

raxStackInit(&it->stack);

}

2.raxSeek

raxStart初始化迭代器后,必须调用raxSeek函数初始化迭代器的位置。

int raxSeek(raxIterator *it, const char *op, unsigned char *ele, size_t len);

·it为raxStart初始化的迭代器。

·op为查找操作符,可以为大于(>)、小于(<)、大于等于(>=)、小于等于(<=)、等于(=)、首个元素(^)、末尾元素($)。

·ele为待查找的key。

·len为ele的长度

 

查找末尾元素可以直接在Rax中找到最右侧的叶子节点,

查找首个元素被转换为查找大于等于空的操作。 return raxSeek(it,">=",NULL,0);

处理大于、小于、等于等操作主要分为以下几步。

  1. 在rax中查找key:

size_t i = raxLowWalk(it->rt,ele,len,&it->node,NULL,&splitpos,&it->stack);

  1. 如果key找到,并且op中设置了等于,则操作完成:

if (eq && i == len && (!it->node->iscompr || splitpos == 0) &&

it->node->iskey)

{

/* 找到该key并且op中设置了=. */

if (!raxIteratorAddChars(it,ele,len)) return 0; /* OOM. */

it->data = raxGetData(it->node);

}

3)如果仅仅设置等于,并没有找到key,则将迭代器的标志位设置为末尾。

4)如果设置了等于但没有找到key,或者设置了大于或者小于符号,则需要继续查找,这一步又分为2步。

①首先将查找key的路径中所有匹配的字符,放入迭代器存储key的数组中:

//将查找过程的最后一个节点放入路径栈

if (!raxStackPush(&it->stack,it->node)) return 0;

for (size_t j = 1; j < it->stack.items; j++) {

raxNode *parent = it->stack.stack[j-1];

raxNode *child = it->stack.stack[j];

if (parent->iscompr) {

if (!raxIteratorAddChars(it,parent->data,parent->size))

return 0;

} else {

raxNode **cp = raxNodeFirstChildPtr(parent);

unsigned char *p = parent->data;

while(1) {

raxNode *aux;

memcpy(&aux,cp,sizeof(aux));

if (aux == child) break;

cp++;

p++;

}

if (!raxIteratorAddChars(it,p,1)) return 0;

}

}

raxStackPop(&it->stack); //将最后一个节点从路径栈中弹出

 

②根据key的匹配情况以及op的参数,在rax中继续查找下一个或者上一个key,此时主要利用的是raxIteratorNextStep、raxIteratorPrevStep两个接口,这两个接口也是raxNext以及raxPrev的核心处理函数,

 

3.raxNext&raxPrev

raxNext与raxPrev为逆操作,高度的相似,此处以raxNext为例;

int raxNext(raxIterator *it) {

if (!raxIteratorNextStep(it,0)) {

errno = ENOMEM;

return 0;

}

if (it->flags & RAX_ITER_EOF) {

errno = 0;

return 0;

}

return 1;

}

 

raxIteratorNextStep函数

int raxIteratorNextStep(raxIterator *it, int noup)

·it 为待移动的迭代器。

·noup 为标志位,可以取0或者1。在raxSeek中,我们有时需要查找比某个key大的下一个key,并且这个待查找的key可能并不存在,此时可能需要将noup设置为1。

raxNext处理过程的重点有3点:

①如果迭代器当前的节点有子节点,则沿着其最左侧的节点一直向下,直到找到下一个key;

②如果当前节点没有子节点,则利用迭代器中的路径栈,依次弹出其父节点,查找父节点是否有其他比当前key大的子节点(迭代器中已经记录了当前的key,通过该值可以进行查找);

③注意noup为1时,我们已经假设迭代器当前节点为上一个key的父节点,故而在路径栈弹出时,第一次需要忽略。

 

while(1) {

int children = it->node->iscompr ? 1 : it->node->size;

if (!noup && children) {

//当前的节点有子节点

if (!raxStackPush(&it->stack,it->node)) return 0;

raxNode **cp = raxNodeFirstChildPtr(it->node);

if (!raxIteratorAddChars(it,it->node->data,

it->node->iscompr ? it->node->size : 1)) return 0;

memcpy(&it->node,cp,sizeof(it->node));

/* 当前节点为key节点, 直接返回. */

.......

} else {

/*当前节点没有子节点,找父节点. */

while(1) {

int old_noup = noup;

/* 已经迭代到rax头部节点,结束 */

    ......

/* 如果当前节点上没有子节点,请尝试父节点的下个子节点。 */

unsigned char prevchild = it->key[it->key_len-1];

if (!noup) { it->node = raxStackPop(&it->stack); }

    else { noup = 0; //第一次弹出父节点的操作被跳过 }

     int todel = it->node->iscompr ? it->node->size : 1;

raxIteratorDelChars(it,todel);

 

/* 如果至少有一个*额外的孩子,请尝试下一个孩子 */

if (!it->node->iscompr && it->node->size > (old_noup ? 0 : 1)) {

raxNode **cp = raxNodeFirstChildPtr(it->node);

int i = 0;

while (i < it->node->size) {

     // 遍历节点所有子节点,找到下一个比当前key大的子节点

if (it->node->data[i] > prevchild) break;

i++; cp++;

}

if (i != it->node->size) {

// 找到了一个子节点比当前key大

raxIteratorAddChars(it,it->node->data+i,1);

if (!raxStackPush(&it->stack,it->node)) return 0;

memcpy(&it->node,cp,sizeof(it->node));

/* 当前节点为key,获取值后返回,不是key则跳出内部while循环 */

    ......

}

}

}

}

}

 

4.raxStop&raxEOF

raxEOF用于标识迭代器迭代结束,raxStop用于结束迭代并释放相关资源 ;

int raxEOF(raxIterator *it) {

return it->flags & RAX_ITER_EOF;

}

/* Free the iterator. */

void raxStop(raxIterator *it) {

if (it->key != it->key_static_string) rax_free(it->key);

raxStackFree(&it->stack);

}

8.4 Stream结构的实现

Stream可以看作是一个消息链表。对一个消息而言,只能新增或者删除,不能更改消息内容,故而本节主要介绍Stream相关结构的初始化以及增删查操作。首先介绍消息流的初始化,之后讲解消息的增删查、消费组的增删查以及消费组中消费者的增删查,最后,介绍如何遍历消息流中的所有消息。

8.4.1 初始化

/* Create a new stream data structure. */

stream *streamNew(void) {

stream *s = zmalloc(sizeof(*s));

s->rax = raxNew();

s->length = 0;

s->last_id.ms = 0;

s->last_id.seq = 0;

s->cgroups = NULL; /* 按需创建以在不使用时节省内存 */

return s;

}

 

图8-18 Stream结构初始化

 

8.4.2 添加元素

任何用户都可以向某个消息流添加消息,或者消费某个消息流中的消息;

 

1.添加消息

Redis提供了streamAppendItem函数,用于向stream中添加一个新的消息:

int streamAppendItem(stream *s, robj **argv, int64_t numfields, streamID *added_id, streamID *use_id)

s 为待插入的数据流:

·argv 为待插入的消息内容,argv[0]为field_1,argv[1]为value_1,依此类推;

·numfields 为待插入的消息的field的总数;

·added_id 不为空,并且插入成功时,将新插入的消息id写入added_id以供调用方使用;

·use_id 为调用方为该消息定义的消息id,该消息id应该大于s中任意一个消息的id。

 

增加消息的流程如下。

①获取rax的最后一个key所在的节点,由于Rax树是按照消息id的顺序存储的,所以最后一个key节点存储了上一次插入的消息。

②查看该节点是否可以插入这条新的消息。

③如果该节点已经不能再插入新的消息(listpack为空或者已经达到设定的存储最大值),在rax中插入新的节点(以消息id为key,新建listpack为value),并初始化新建的listpack;

如果仍然可以插入消息,则对比插入的消息与listpack中的master消息对应的fields是否完全一致,完全一致则表明该消息可以复用master的field。

④将待插入的消息内容插入到新建的listpack中或者原来的rax的最后一个key节点对应的listpack中,这一步主要取决于前2步的结果。

该函数主要是利用了listpack以及rax的相关接口。

 

2.新增消费组

通过streamCreateCG为消息流新增一个消费组,以消费组的名称为key,该消费组的streamCG结构为value,放入rax中;

streamCG *streamCreateCG(stream *s, char *name, size_t namelen, streamID *id) {

// 如果当前消息流尚未有消费组,则新建消费组

if (s->cgroups == NULL) s->cgroups = raxNew();

// 查看是否已经有该消费组,有则新建失败

if (raxFind(s->cgroups,(unsigned char*)name,namelen) != raxNotFound)

return NULL;

// 新建消费组,并初始化相关变量

streamCG *cg = zmalloc(sizeof(*cg));

cg->pel = raxNew();

cg->consumers = raxNew();

cg->last_id = *id;

// 将该消费组插入到消息流的消费组树中, 以消费组的名称为key, 对应的streamCG为value

raxInsert(s->cgroups,(unsigned char*)name,namelen,cg,NULL);

return cg;

}

3.新增消费者

Stream允许为某个消费组增加消费者,但没有直接提供在某个消费组中创建消费者的接口,而是在查询某个消费组的消费者时,发现该消费组没有该消费者时选择插入该消费者,该接口在8.4.4节进行介绍。

 

8.4.3 删除元素

如何从消息流中删除消息以及限制消息流的大小。如何释放消费组中的消费者以及如何释放整个消费组。

1.删除消息

streamIteratorRemoveEntry函数用于移除某个消息,值得注意的是,该函数通常只是设置待移除消息的标志位为已删除,并不会将该消息从所在的listpack中删除。当消息所在的整个listpack的所有消息都已删除时,则会从rax中释放该节点。

 

void streamIteratorRemoveEntry(streamIterator *si, streamID *current) {

unsigned char *lp = si->lp;

int64_t aux;

int flags = lpGetInteger(si->lp_flags);

flags |= STREAM_ITEM_FLAG_DELETED;

lp = lpReplaceInteger(lp,&si->lp_flags,flags); // 设置消息的标志位

 

/* Change the valid/deleted entries count in the master entry. */

unsigned char *p = lpFirst(lp);

aux = lpGetInteger(p);

if (aux == 1) {

/* 当前Listpack只有待删除消息,可以直接删除节点. */

lpFree(lp);

raxRemove(si->stream->rax,si->ri.key,si->ri.key_len,NULL);

} else {

/* 修改listpack master enty中的统计信息 */

lp = lpReplaceInteger(lp,&p,aux-1);

p = lpNext(lp,p); /* Seek deleted field. */

aux = lpGetInteger(p);

lp = lpReplaceInteger(lp,&p,aux+1);

/* 查看listpack是否有变化(listpack中元素变化导致的扩容缩容) . */

if (si->lp != lp)

raxInsert(si->stream->rax,si->ri.key,si->ri.key_len,lp,NULL);

}

.....

}

2.裁剪消息流

就是将消息流的大小(未删除的消息个数,不包含已经删除的消息)裁剪到给定大小,删除消息时,按照消息id,从小到大删除。该接口为streamTrimByLength:

// stream为待裁剪的消息流; maxlen为消息流中最大的消息个数; approx为是否可以存在偏差

int64_t streamTrimByLength(stream *s, size_t maxlen, int approx) ;

 

对于消息流的裁剪,主要有以下几点。

1)消息删除是按照消息id的顺序进行删除的,先删除最先插入(即消息id最小的)消息。

2)从效率的角度上说,函数调用时最好加上approx标志位。

具体实现过程

1)获取stream的Rax树的第一个key所在的节点:

if (s->length <= maxlen) return 0; // stream中的消息个数小于maxlen,不需要删除

raxIterator ri; // 初始化rax迭代器

raxStart(&ri,s->rax);

raxSeek(&ri,"^",NULL,0);

int64_t deleted = 0; // 统计已经删除的消息个数

2)遍历rax树的节点,不断删除消息,直到剩余消息个数满足要求:

while(s->length > maxlen && raxNext(&ri)) {

// 遍历Rax树删除消息直到满足要求

}

3)具体删除消息的部分可以分为如下几步。

·查看是否需要删除当前节点,如果删除该节点存储的全部消息后仍然未达到要求,则删除该节点。

·不需要删除该节点存储的全部消息,如果函数参数中设置了"approx",则不再进行处理,可以直接返回。 // if (approx) break;

·不需要删除该节点的全部消息,则遍历该节点存储的消息,将部分消息的标志位设置为已经删除。

 

删除当前节点代码

if (s->length - entries >= maxlen) { // 需要删除该节点的全部消息

lpFree(lp);

raxRemove(s->rax,ri.key,ri.key_len,NULL); // 调用Rax的接口删除key

raxSeek(&ri,">=",ri.key,ri.key_len);

s->length -= entries;

deleted += entries;

continue;

}

 

遍历当前节点的消息,将其部分消息设置为已删除 代码

while(p) { // 遍历该节点存储的全部消息,依次删除,直到消息个数满足要求

int flags = lpGetInteger(p);

int to_skip;

/* Mark the entry as deleted. */

if (!(flags & STREAM_ITEM_FLAG_DELETED)) {

flags |= STREAM_ITEM_FLAG_DELETED;

lp = lpReplaceInteger(lp,&p,flags);

deleted++;

s->length--;

if (s->length <= maxlen) break; /* Enough entries deleted. */

}

// 移动到下一个消息 ....

}

 

3.释放消费组

接口为streamFreeCG,该接口主要完成2部分内容,首先释放该消费组的pel链表,之后释放消费组中的每个消费者。

/* Free a consumer group and all its associated data. */

void streamFreeCG(streamCG *cg) {

// 删除该消费组的pel链表,释放时设置回调函数用于释放每个消息对应的streamNACK结构

raxFreeWithCallback(cg->pel,(void(*)(void*))streamFreeNACK);

// 释放每个消费者时,需要释放该消费者对应的streamConsumer结构

raxFreeWithCallback(cg->consumers,(void(*)(void*))streamFreeConsumer);

zfree(cg);

}

/* Free a NACK entry(未确认). */

void streamFreeNACK(streamNACK *na) {

zfree(na);

}

 

4.释放消费者

需要注意的是,不需要释放该消费者的pel,因为该消费者的未确认消息结构streamNACK是与消费组的pel共享的,直接释放相关内存即可。

void streamFreeConsumer(streamConsumer *sc) {

raxFree(sc->pel); /*此处仅仅是将存储streamNACK的Rax树释放 /

sdsfree(sc->name);

zfree(sc);

}

 

8.4.4 查找元素

查找消息、查找消费组、查找消费组中的消费者 ;

(1)查找消息

Stream查找消息是通过迭代器实现的,这部分内容我们将在8.4.5节进行介绍。

(2)查找消费组

Redis提供了streamLookupCG接口用于查找Stream的消费组,该接口较为简单,主要是利用Rax的查询接口:

streamCG *streamLookupCG(stream *s, sds groupname) {

if (s->cgroups == NULL) return NULL;

streamCG *cg = raxFind(s->cgroups,(unsigned char*)groupname,

sdslen(groupname));

return (cg == raxNotFound) ? NULL : cg;

}

 

(3)查找消息组中的消费者

streamLookupConsumer接口用于查询某个消费组中的消费者。消费者不存在时,可以选择是否将该消费者添加进消费组。

/*在消费组cg中查找消费者name; 如果没有查到并且create为1时,将该消费者加入消费组 */

streamConsumer *streamLookupConsumer(streamCG *cg, sds name, int flags) {

int create = !(flags & SLC_NOCREAT);

int refresh = !(flags & SLC_NOREFRESH);

streamConsumer *consumer = raxFind(cg->consumers,(unsigned char*)name,

sdslen(name));

if (consumer == raxNotFound) {

if (!create) return NULL; // 不需要插入

consumer = zmalloc(sizeof(*consumer));

consumer->name = sdsdup(name);

consumer->pel = raxNew();

raxInsert(cg->consumers,(unsigned char*)name,sdslen(name),

consumer,NULL);

}

if (refresh) consumer->seen_time = mstime(); //已经查询到该消费者,更新时间戳

return consumer;

}

 

8.4.5 遍历

Stream的迭代器streamIterator,用于遍历Stream中的消息,相关的接口主要有以下4个:

// 用于初始化迭代器,值得注意的是,需要指定迭代器的方向。

void streamIteratorStart(streamIterator *si, stream *s, streamID *start, streamID *end, int rev);

//streamIteratorGetID与streamIteratorGetField配合使用,用于遍历所有消息的所有field-value

int streamIteratorGetID(streamIterator *si, streamID *id, int64_t *numfields);

void streamIteratorGetField(streamIterator *si, unsigned char **fieldptr, unsigned char **valueptr, int64_t *fieldlen, int64_t *valuelen);

// 释放迭代器的相关资源。

void streamIteratorStop(streamIterator *si);

 

1)streamIteratorStart接口

接口负责初始化streamIterator。它的具体实现主要是利用Rax提供的迭代器:

void streamIteratorStart(streamIterator *si, stream *s, streamID *start, streamID *end, int rev) {

.....

/* 在基数树中寻找正确的节点. */

raxStart(&si->ri,s->rax);

if (!rev) { // 正向迭代器

if (start && (start->ms || start->seq)) { // 设置了开始的消息id

raxSeek(&si->ri,"<=",(unsigned char*)si->start_key,

sizeof(si->start_key));

if (raxEOF(&si->ri)) raxSeek(&si->ri,"^",NULL,0);

} else {

raxSeek(&si->ri,"^",NULL,0); // 默认情况为指向Rax树中第一个key所在的节点

}

} else { // 逆向迭代器

if (end && (end->ms || end->seq)) {

raxSeek(&si->ri,"<=",(unsigned char*)si->end_key,

sizeof(si->end_key));

if (raxEOF(&si->ri)) raxSeek(&si->ri,"$",NULL,0);

} else {

raxSeek(&si->ri,"$",NULL,0);

}

}

si->stream = s;

si->lp = NULL; /* There is no current listpack right now. */

si->lp_ele = NULL; /* Current listpack cursor. */

si->rev = rev; /* Direction, if non-zero reversed, from end to start. */

}

 

2)streamIteratorGetID接口

该接口负责获取迭代器当前的消息id,可以分为以下2步。

①查看当前所在的Rax树的节点是否仍然有其他消息,没有则根据迭代器方向调用Rax迭代器接口向前或者向后移动。

②在rax key对应的listpack中,查找尚未删除的消息,此处需要注意streamIterator的指针移动。

  1. streamIteratorGetField接口

直接使用该迭代器内部的指针,获取当前消息的field-value对:

void streamIteratorGetField(streamIterator *si, unsigned char **fieldptr, unsigned char **valueptr, int64_t *fieldlen, int64_t *valuelen) {

if (si->entry_flags & STREAM_ITEM_FLAG_SAMEFIELDS) {

// 当前消息的field内容与master_fields一致,读取master_field域内容

*fieldptr = lpGet(si->master_fields_ptr,fieldlen,si->field_buf);

si->master_fields_ptr = lpNext(si->lp,si->master_fields_ptr);

} else { // 直接获取当前的field, 移动lp_ele指针

*fieldptr = lpGet(si->lp_ele,fieldlen,si->field_buf);

si->lp_ele = lpNext(si->lp,si->lp_ele);

}

// 获取field对应的value,并将迭代器lp_ele指针向后移动

*valueptr = lpGet(si->lp_ele,valuelen,si->value_buf);

si->lp_ele = lpNext(si->lp,si->lp_ele);

}

 

  1. streamIteratorStop接口

主要利用raxIterator接口释放相关资源:

void streamIteratorStop(streamIterator *si) {

raxStop(&si->ri);

}

 

8.5 本章小结

本章主要介绍了Stream的底层实现。首先讲解了Stream结构需要依赖的两种数据结构Listpack以及Rax,并详细介绍了这两种结构的基本操作。之后,进一步说明了Stream是如何利用这两种结构的。

 

标签:Stream,Redis5,char,源码,消息,key,listpack,节点,rax
来源: https://www.cnblogs.com/coloz/p/13812840.html