一. What
Redis5带来的最大的改变应该就是引入了stream这个数据结构了。这就相当于在Redis里面内置了一个Kafka。
Redis5里面的stream底层是基于基数树实现的。要想深入要就stream的实现,就必须先搞懂基数树的实现。
那么什么是基数树呢?
TOTO
二. How
因为这篇博客是单独分析Redis里面的基数树的实现,所以把基数树相关的实现单独拎出来。
基数树相关的实现在rax.h、rax.c、rax_malloc.h和zmalloc.c以及zmalloc.h这几个文件中。
用CLion新建一个C工程,把上面几个文件复制到工程里面去,并需要在在CMakeLists.txt文件中引用这几个文件:
aux_source_directory(. SOURCE1)
add_executable(rax ${SOURCE1})
然后需要把zmalloc.h文件中的#include "config.h"这行代码注释掉。
#include
#include "rax.h"
int main() {
rax* tree = raxNew();
/* 插入元素*/
raxInsert(tree,"ANNIBALE",8,"value1",NULL);
raxInsert(tree,"SCO",3,"value2",NULL);
raxInsert(tree,"ANNIBALI",8,"value3",NULL);
raxInsert(tree,"AGO",3,"value4",NULL);
raxInsert(tree,"CIAO",4,"value4",NULL);
raxInsert(tree,"ANNI",4,"value5",NULL);
/* 查找元素*/
int result = raxFind(tree,"AG0",3);
/* 移除元素*/
raxRemove(tree,"CIAO",4,NULL);
/* 释放树*/
raxFree(tree);
return 0;
}
三. Why
2. 基础数据结构
先来看下相关的数据结构的定义。
①. raxNode
首先是基数树的节点raxNode,定义如下:
typedef struct raxNode {
uint32_t iskey:1; /* 该节点是否包含key,占用1位字节*/
uint32_t isnull:1; /* 该节点对应的value是否为null(如果是null的话就不用分配内存存储空数据),占用1位字节*/
uint32_t iscompr:1; /* 该节点是否是压缩节点,占用1位字节*/
uint32_t size:29; /* 如果该是压缩节点,则size保存字符串的长度,如果是非压缩节点,size则保存子节点的数量,占用29位字节*/
unsigned char data[]; // 柔性数组,保存节点对应的数据(子节点指针,key字符串),不占用字节
} raxNode;
- iskey:该字段表示当前节点是否是一个完整的key。一个raxNode只有在该节点是key的时候才会保存对应的value。该字段占用一个字节。
- isnull:该字段表示节点对应的value是否为null(只有在iskey=1 的情况下节点上才会保存value)。如果是null的话则可以不用分配内存保存value,节省内存。该字段占用一个字节。
- iscompr: 该节点是否是压缩节点。
- size:如果节点是压缩节点,则该字段保存压缩字符串的长度,如果是正常非压缩节点,则该字段保存子节点的个数。
- **data:数据域,用来保存对应的字符串,为指针对齐填充的字节,指向子节点的指针以及指向value的指针(如果是key的话则存在)。
raxNode节点可以分为两种:
- 压缩节点。
- 正常节点(非压缩节点)。
所谓压缩节点是指该节点中保存的数据是在同一个字符串中连续存在的。
正常节点(非压缩节点)是指节点中保存的数据是分属于不同的字符串的,即每个字符来自于不同的字符串。
对于正常非压缩节点来说,节点中会保存size个指向子节点的指针。对于压缩节点来说,节点只会有一个子节点,也就只会保存一个指向子节点的指针。
假设节点中保存的字符串是abc,且该节点iskey=1,对应的value="myvalue",对于正常非压缩节点来说,布局如下:
对于压缩节点来说,布局如下:
当然,如果节点iskey=0或者iskey=1但是isnull=1,则不会有指向value的指针。
上面的图已经展示了,在data域中会保存4种数据:
- 表示key的字符(串)。其中每个字符占用一个字节,并且按照字典顺序排序。
- 内存对齐字节填充。
至于为什么需要内存对齐,这又是另外一个深奥的问题,本质上来说就是提高cpu access memory的性能。在我们这个场景中,字节对齐填充发生在key字符(串)与保存指向子节点指针之间,要求填充之后的数据的首地址按照sizeof(void)字节对齐(sizeof(void)的含义是获取一个指针的大小,指针的本质就是内存地址,结果取决于编译器目标平台的ABI,与目标平台的内存空间有关,对于32位的系统来说sizeof(void)=4,对于64位系统来说sizeof(void)=8),也就是说raxNode的header加上size个字节需要填充到sizeof(void*)的整数倍长度,字节对齐需要补充的字节数的计算公式为
#define raxPadding(nodesize) ((sizeof(void*)-((nodesize+4) % sizeof(void*))) & (sizeof(void*)-1))
其中nodesize为当前节点的key的字符数量。
- 指向字节点的指针。对于压缩节点来说,只会有一个子节点,因此只会有一个指向子节点的指针。对于正常非压缩节点来说,会有size个子节点,所以会有size个指向子节点的指针。
- 如果该节点iskey=1,则子节点后面还会有个指针,指向具体的value。
根据上面的分析,可以很明显的知道,一个raxNode所占的内存空间包括5部分:
- 头部。占用字节大小为sizeof(raxNode)(恒等于4:1+1+1+29=32位)。
- size个字符。占用字节大小为size。
- 为了内存对齐填充的字节。占用字节大小为((sizeof(void)-((nodesize+4) % sizeof(void))) & (sizeof(void*)-1))。
- 指向子节点的指针。占用字节大小为node->iscompr ? sizeof(raxNode*) : size * sizeof(raxNode *)。
- 指向value的指针。占用字节为node->iskey ? sizeof(void*) : 0。
所以,对于指定的节点n所占用的内存大小计算公式为:
#define raxNodeCurrentLength(n) ( \
sizeof(raxNode)+(n)->size+ \
raxPadding((n)->size)+ \
((n)->iscompr ? sizeof(raxNode*) : sizeof(raxNode*)*(n)->size)+ \
(((n)->iskey && !(n)->isnull)*sizeof(void*)) \
)
获取节点中指向第一个子节点的指针的计算公式为:
#define raxNodeFirstChildPtr(n) ((raxNode**) ( \
(n)->data + \
(n)->size + \
raxPadding((n)->size)))
从上面的分析也很容易得出节点中保存指向最后一个子节点的指针计算公式为:
#define raxNodeLastChildPtr(n) ((raxNode**) ( \
((char*)(n)) + \
raxNodeCurrentLength(n) - \
sizeof(raxNode*) - \
(((n)->iskey && !(n)->isnull) ? sizeof(void*) : 0) \
))
因为是要获取指向最后一个子节点的指针的首地址,所以需要减掉sizeof(raxNode*)。
②. rax
基数树的定义如下:
typedef struct rax {
raxNode *head; // 基数树的根节点
uint64_t numele; // 元素的数量
uint64_t numnodes; // 节点的数量
} rax;
这个结构比较简单。
插入一个元素,可能会生成多个raxNode节点。因此numele >= numnodes。
在radix tree中,如果连续的节点都是只有一个子节点,则这些节点可以被压缩成一个压缩节点。
③. raxStack
typedef struct raxStack {
void **stack; /* Points to static_items or an heap allocated array. */
size_t items, maxitems; /* Number of items contained and total space. */
/* Up to RAXSTACK_STACK_ITEMS items we avoid to allocate on the heap
* and use this static array of pointers instead. */
void *static_items[RAX_STACK_STATIC_ITEMS];
int oom; /* True if pushing into this stack failed for OOM at some point. */
} raxStack;
在对树进行遍历的时候,可以
2. API
rax主要对外提供以下几个接口:
// 新建一个rax
rax *raxNew(void);
// 往rax上面插入一个新的元素,s指向key,data指向value,如果key已存在,则更新
int raxInsert(rax *rax, unsigned char *s, size_t len, void *data, void **old);
// 往rax上面插入一个新的元素,如果key已经存在,则直接返回,不做更新
int raxTryInsert(rax *rax, unsigned char *s, size_t len, void *data, void **old);
int raxRemove(rax *rax, unsigned char *s, size_t len, void **old);//移除一个元素
void *raxFind(rax *rax, unsigned char *s, size_t len);// 查找key
void raxFree(rax *rax); // 释放rax
void raxFreeWithCallback(rax *rax, void (*free_callback)(void*)); // 带回调的free
写在最后
- 第一:看完点赞,感谢您的认可;
- ...
- 第二:随手转发,分享知识,让更多人学习到;
- ...
- 第三:记得点关注,每天更新的!!!
- ...