Redis基数树:rax前缀树如何提升键值存储效率
为什么Redis需要特殊的数据结构?
当你在Redis中执行KEYS user:*命令时,是否好奇它如何高效地查找所有匹配前缀的键?这背后正是Redis基数树(Radix Tree)——一种被称为rax的特殊前缀树结构在发挥作用。不同于普通树结构,基数树特别适合处理大量字符串键的存储与检索,这也是Redis能在百万级键值对中快速定位数据的关键原因之一。
什么是rax基数树?
rax(Redis Radix Tree)是Redis自主实现的压缩前缀树,它结合了前缀树(Trie)和基数树的优点,通过路径压缩技术减少内存占用,同时保持高效的查找性能。在src/rax.h中定义了其核心结构:
typedef struct raxNode {
uint32_t iskey:1; /* 节点是否包含键 */
uint32_t isnull:1; /* 值是否为NULL */
uint32_t iscompr:1; /* 节点是否被压缩 */
uint32_t size:29; /* 子节点数量或压缩字符串长度 */
unsigned char data[]; /* 柔性数组存储字符和子节点指针 */
} raxNode;
typedef struct rax {
raxNode *head; /* 根节点 */
uint64_t numele; /* 元素总数 */
uint64_t numnodes; /* 节点总数 */
void *metadata[]; /* 元数据 */
} rax;
核心特性:压缩与高效并存
1. 路径压缩技术
普通前缀树在存储类似"user:1000"、"user:1001"这样的键时会产生大量冗余节点。rax通过压缩连续单分支路径解决这一问题。例如,存储"foo"、"foobar"和"footer"时,rax会将公共前缀"foo"压缩为单个节点:
["foo"] ""
|
[t b] "foo"
/ \
"er" "ar"
| |
[] []
footer foobar
这种结构在src/rax.h的注释中有详细说明,通过iscompr标记和size字段控制压缩状态。
2. 混合节点类型
rax节点分为两种类型:
- 压缩节点(
iscompr=1):存储连续字符序列,如上述示例中的"foo" - 非压缩节点(
iscompr=0):存储分支字符集合,如上述示例中的[t,b]
这种混合结构在src/rax.c的raxLowWalk函数中得到体现,通过不同的遍历逻辑处理两种节点:
if (h->iscompr) {
// 处理压缩节点:遍历连续字符
for (j = 0; j < h->size && i < len; j++, i++) {
if (v[j] != s[i]) break;
}
} else {
// 处理非压缩节点:查找分支字符
for (j = 0; j < h->size; j++) {
if (v[j] == s[i]) break;
}
}
实战应用:Redis中的rax场景
1. 键空间遍历
当执行KEYS pattern或SCAN命令时,Redis使用rax的迭代器功能。在src/defrag.c中可以看到遍历实现:
raxIterator ri;
raxStart(&ri, s->rax);
raxSeek(&ri, "^", NULL, 0);
while (raxNext(&ri)) {
// 处理每个键值对
raxSetData(ri.node, ri.data = newdata);
}
raxStop(&ri);
2. 流数据结构(Stream)
Redis Stream使用rax存储消费者组信息。在src/defrag.c中,defragStreamConsumerGroup函数展示了如何操作流中的rax结构:
void* defragStreamConsumerGroup(raxIterator *ri, void *privdata) {
streamCG *cg = ri->data;
// 递归处理消费者组的pel(挂起条目列表)
defragRadixTree(&cg->pel, 1, defragStreamConsumerPendingEntry, NULL);
return cg;
}
3. 模块系统支持
Redis模块可以通过API使用rax结构。src/module.c中的RedisModule_Dict实现就基于rax:
d->rax = raxNew(); // 创建新的rax
raxInsert(d->rax, key, keylen, ptr, NULL); // 插入元素
raxFind(d->rax, key, keylen, &res); // 查找元素
性能对比:为什么选择rax而非其他结构?
| 数据结构 | 查找复杂度 | 内存效率 | 前缀匹配 | Redis应用场景 |
|---|---|---|---|---|
| 哈希表 | O(1) | 低(无压缩) | 不支持 | 主键空间 |
| 普通前缀树 | O(k) | 低(冗余节点) | 支持 | - |
| rax基数树 | O(k) | 高(路径压缩) | 支持 | 键扫描、Stream、模块 |
(k为键的长度)
源码探索:核心操作实现
1. 插入操作
raxInsert函数(src/rax.c)处理节点分裂与压缩逻辑:
int raxGenericInsert(rax *rax, unsigned char *s, size_t len, void *data, void **old, int overwrite) {
size_t i = raxLowWalk(rax, s, len, &h, &parentlink, &j, NULL);
// 处理分裂压缩节点的复杂逻辑
// ...
rax->numele++;
return 1;
}
2. 迭代器实现
rax提供了安全迭代器,支持在遍历过程中修改树结构。src/rax.h定义了迭代器结构:
typedef struct raxIterator {
int flags; /* 迭代器状态标志 */
rax *rt; /* 关联的rax树 */
unsigned char *key; /* 当前键 */
void *data; /* 当前数据 */
// ... 其他字段
} raxIterator;
使用示例:
raxIterator iter;
raxStart(&iter, rax); // 初始化迭代器
raxSeek(&iter, "^", NULL, 0); // 定位到起始位置
while (raxNext(&iter)) { // 遍历所有元素
printf("key: %.*s\n", (int)iter.key_len, iter.key);
}
raxStop(&iter); // 释放迭代器
总结:rax如何赋能Redis
rax基数树通过路径压缩、混合节点类型和高效迭代三大特性,为Redis提供了兼顾内存效率和查询性能的数据结构。它不仅优化了KEYS、SCAN等命令的实现,还为Stream、模块系统等高级特性提供了底层支持。
要深入了解rax的实现细节,可以阅读以下源码文件:
- src/rax.h:数据结构定义
- src/rax.c:核心算法实现
- src/defrag.c:碎片整理中的应用
- src/module.c:模块API封装
rax的设计展示了Redis在追求极致性能道路上的匠心独运,也为我们在处理字符串集合问题时提供了优秀的参考范例。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



