当前位置: 首页 > news >正文

Redis 64字节分界线与跳表实现原理 - 实践

文章目录

  • 一、为什么 Redis 把 “64 字节” 作为字符串分界线
    • 1.1 Redis 字符串底层是 SDS
    • 1.2 SDS 的头部开销
    • 1.3 柔性数组
    • 1.4 redisObject 的内存占用
    • 1.5 embstr 与 raw 两种编码
    • 1.6 为什么阈值是 44 字节?
  • 二、Redis 跳表
    • 2.1 什么是跳表
    • 1.2 为什么需要跳表
    • 1.3 随机层数
    • 1.4 Redis 中跳表的具体实现特点
    • 1.5 跳表在 Redis 有序集合(Zset)中的应用

一、为什么 Redis 把 “64 字节” 作为字符串分界线

面试参考回答

  • Redis 在处理字符串时,区分了 embstrraw 两种编码。embstr 就是 embedded string(嵌入字符串),它把 robj 、字符串数据 SDS 头和字符串内容,放在同一块内存里,一次性分配。相比之下,raw 方式下 robj 只维护一个指针,实际的字符串数据在堆上另行分配。对于短字符串,Redis 采用 embstr 可以减少一次 malloc/free,避免指针跳转,访问效率更高。

  • 为什么选 64 字节 作为边界?这是因为常见的内存分配器都会按 2 的幂次分配小块内存,64B 是一个典型的分配粒度。同时,现代 CPU 缓存行的最小访问单位通常也是 64B。把 robj 和字符串内容打包在 64B 内,可以让对象元数据和字符串落在同一缓存行里,提升缓存命中率和访问局部性。

  • 为什么阈值是 44 字节?在 64B 里,robj 占 16B,SDS 头占 3B,结尾 \0 占 1B,剩下 44B 可用于存放字符串内容。因此,Redis 把 44 字节作为 embstr 与 raw 的分界线。短字符串用 embstr,性能和空间利用率最好;超过 44B 时,就用 raw,把字符串单独放在堆上,更灵活。

1.1 Redis 字符串底层是 SDS

在 Redis 中,字符串不是用传统的 char* 来管理,而是用 SDS(Simple Dynamic String)

SDS 的核心特点是用“长度信息”而不是 \0 来描述字符串,这样就支持二进制安全的字符串,像图片、视频、字节流等都可以存放,不会因为遇到 \0 而被截断。同时,SDS 在字符串结尾依然保留一个 \0,这样也能兼容 C 标准库的字符串函数。

1.2 SDS 的头部开销

SDS 有不同的头部类型(如 sdshdr5/8/16/32/64),根据字符串长度不同选择合适的头部,以节省空间。
短字符串常用 sdshdr8,它的结构大致如下(packed 后不会有额外填充):

struct __attribute__((__packed__)) sdshdr8 {
uint8_t len;
// 已用长度,1 字节
uint8_t alloc;
// 总分配大小(不含 header 和 '\0'),1 字节
unsigned char flags;
// 标志(1 字节)
char buf[];
// 字符数据(后面紧接)
};

这个头部占用 3 个字节(len + alloc + flags = 3),紧随其后的 buf[] 用来放实际字符。这里 buf[]是柔性数组,柔性数组 buf[] 不占用结构体本身空间,真正的大小要靠 malloc 分配。

1.3 柔性数组

柔性数组(Flexible Array Member,简称 FAM)必须定义在结构体中,并且必须放在结构体的最后一个成员。结构体不能只包含柔性数组,必须至少有一个普通成员,否则编译器会报错。

  • sizeof(结构体)不包含柔性数组的大小,仅仅计算到柔性数组前为止。
  • 在上例中,sizeof(struct sdshdr8) 只包含 len + alloc + flags 的大小,不包括 buf[]
  • 柔性数组成员只是个“占位符”,本身不占空间。

如果要使用柔性数组,需要额外分配一段内存:

// 分配一个带 128 字节 buf 的 sdshdr8
struct sdshdr8 *sh = malloc(sizeof(struct sdshdr8) + 128);
  • sizeof(struct sdshdr8):是结构体本身大小,不包括 buf[]
  • +128:给柔性数组 buf[] 分配 128 字节空间。
  • 此时 sh->buf 就可以直接使用了。

取出数据有两种常见方式:

1. 直接用结构体指针访问

sh->buf[0] = 'A';

因为 buf[] 的起始地址已经包含在 sh 里面。

2. 如果先拿到 char *,再取结构体

比如 Redis 的 sds 实现:

char *sds = (char*)sh->buf;
// 用户只看到字符串部分

那么要取回 struct sdshdr8,需要回退

struct sdshdr8 *sh = (void*)(sds - sizeof(struct sdshdr8));

如果结构体中使用 char *buf 指针成员而不是柔性数组 buf[],那么在申请内存时需要先为结构体本身分配一次,再为指针指向的缓冲区分配一次,释放时也要对应执行两次 free,这样既繁琐又容易造成内存碎片;而如果使用柔性数组 char buf[],则只需一次 malloc 就能同时为结构体和数组申请连续内存空间,并且只需一次 free 即可释放,效率更高,内存管理更简单。

1.4 redisObject 的内存占用

Redis 中的字符串对象需要一个 redisObject(简称 robj)来描述。 在典型的 64 位环境下,robj 大约占 16 字节,主要字段有:

typedef struct redisObject {
unsigned type:4;
// 位域:type, encoding, lru 等合起来通常占 4 字节
unsigned encoding:4;
unsigned lru:24;
// 假设合并为 4 字节总和
int refcount;
// 4 字节
void *ptr;
// 64-bit 系统为 8 字节
} robj;
// 合计:4 + 4 + 8 = 16 字节(在典型 64-bit 编译器布局下)

1.5 embstr 与 raw 两种编码

Redis 为了存储字符串,提供了两种方式:

  • embstr:把 robj、SDS header 和字符串内容一次性分配到同一块连续内存。这样只需一次 malloc,减少指针跳转,性能更好。
  • rawrobj 本身只保存一个指针,字符串内容单独在堆上分配。适合大字符串,因为可以灵活扩展。

区别在于:短字符串用 embstr,更高效;长字符串用 raw,更灵活。

在这里插入图片描述

对于短字符串,embstr 可以减少一次内存分配、减少指针跳转、提升缓存局部性与性能;但当字符串比较大时,把数据也放在 robj 所在的那块内存会使得一次分配变得很大且可能浪费内存/改变分配器行为,因此超过阈值 44 字节就改用 raw。

1.6 为什么阈值是 44 字节?

现代 CPU 的缓存行大小通常是 64 字节,把 robj + SDS header + 字符串内容 + 结尾 \0 全部放在 64 字节内。

在这里插入图片描述
逐步计算:

  1. 缓存行大小:64 字节
  2. 减去 robj16B:64 − 16 = 48
  3. 减去 SDS 头 3B:48 − 3 = 45
  4. 再减去结尾 \0为 1B:45 − 1 = 44

所以,实际能存放的字符串长度是 44 字节
因此,Redis 规定:字符串长度 ≤44 时,用 embstr;超过时用 raw

虽然 SDS 用 len 表示字符串长度,但它仍在末尾保留一个 \0,方便和 C 库函数兼容。
这个 \0 会占用一个字节,所以要在计算时专门减掉。


二、Redis 跳表

Redis 的有序集合需要既能快速按分数排序,又能高效插入删除。普通链表或数组都有缺陷,B+树实现复杂,所以 Redis 选择了跳表。跳表通过“多层级的有序链表”加速查找,查找、插入、删除的期望复杂度都是 O(log n),实现比树简单。
Redis 使用概率方式决定节点的层数,不需要频繁重建结构,还把最高层数限制为 32。
在 zset 里,小数据用紧凑编码节省内存,大数据用“跳表+哈希表”组合:哈希表用于按成员名快速查找,跳表用于按分数排序和范围查询。

2.1 什么是跳表

跳表可以理解为一种对传统链表进行优化的数据结构,它通过建立多级“索引”或“快速通道”来加速查找过程。用空间换时间:通过上层稀疏索引快速跨越大量节点,再逐层下降到更精细的层次,最终在底层链表完成精确定位。

一个理想化的例子:
假设一个有序链表存储着数据 1, 3, 7, 9, 12, 17。如果我们要查找数字 9,在普通链表中只能从头开始逐个比较,需要 4 次。而在一个理想的多层级跳表中,查找路径可能是这样的:

在这里插入图片描述

这种查找方式比单链表高效很多,因为减少了遍历步数。

1.2 为什么需要跳表

Redis 使用跳表主要是为了其有序集合(Sorted Set / Zset) 的实现。Zset 需要支持两种核心操作:

  1. 范围查询:快速获取指定分数区间的成员,例如 ZRANGEBYSCORE
  2. 高效更新:能够快速地进行插入、删除和修改。

传统的数据结构在面对这些需求时有明显短板:

  • 有序数组:查找快(二分查找,O(log n)),但插入和删除需要移动大量元素,非常慢(O(n))。
  • 普通有序链表:插入和删除快(O(1)),但查找效率极低(O(n)),因为只能从头到尾遍历。

跳表则是在链表的基础上,通过增加多级索引,以空间换时间,实现了接近二分查找的效率,同时保留了链表插入删除灵活的优点。

1.3 随机层数

上文设想的理想跳表结构(比如严格每两个节点建立一个索引)在理论上很高效。但在现实中,每次插入或删除一个节点,都可能破坏原有完美的索引层级关系,如果需要重新构建整个索引结构,成本将非常高,使得更新操作变得很慢。

在这里插入图片描述

为了避免维护理想结构的高昂代价,Redis 和大多数跳表实现采用了一种巧妙的概率化方法来决定每个新插入节点的层级:

这种方式虽然使得跳表在局部上不是最完美的形态,但从统计学的宏观角度来看,其结构性能依然非常接近理想跳表。最大的好处是插入和删除操作变得非常简单高效,只需要调整相邻节点指针,而无需重构整个索引。

1.4 Redis 中跳表的具体实现特点

Redis 对跳表的实现进行了一些工程上的优化:

  1. 最大层级限制:Redis 将跳表的最大层级限制为 32。即使随机算法理论上可能生成很高的层数,这个限制也能防止在极端情况下消耗过多内存。

  2. 内存与性能的平衡:通过调整随机算法的参数,Redis 让跳表的整体结构相对“扁平”一些,平均层数较低。虽然查找性能略有牺牲,但显著节约了内存使用,整体上更适合内存数据库的需求。

1.5 跳表在 Redis 有序集合(Zset)中的应用

Zset 会根据元素数量和成员字符串长度智能选择更节省内存的底层表示,从而在内存占用和操作效率之间做出平衡。Redis 的有序集合有两种底层实现:

切换条件是:

跳表 + 字典的组合,结合了两种数据结构的优势,既能快速定位成员,又能支持排序和范围操作。:

http://www.hskmm.com/?act=detail&tid=27931

相关文章:

  • 新手报道
  • VUE---await的运用
  • 供应链业务架构设计概览
  • VS Code保存.vue文件自动格式化标签的问题
  • 基于最小二乘(LS)信道估计的MATLAB实现
  • 让老弟做个数据同步,结果踩了 7 个大坑!
  • 2025焊接件加工制造厂家口碑最新推荐榜:实力工艺与市场口碑
  • 2025机械加工厂家实力排行榜:技术精度与供货效率权威测评
  • 2025 年最新推荐!依托优质运输网络的国际搬家海运公司排行榜:覆盖澳洲多地家具海运需求澳洲/悉尼/墨尔本/大型家具海运公司推荐
  • 完整教程:计算机环境、用户与系统变量
  • 2025耐磨轮胎厂家TOP5推荐:超强抓地力与持久耐用性深度
  • CF做题记录
  • 2025 年中国搬家服务公司最新推荐榜:聚焦海运移民家具运输等需求,精选优质企业实测解析国际/国际海运/国际移民/家具海运/回国搬家海运公司推荐
  • NVIDIA CUDA 镜像 Docker 容器化部署全流程
  • AI时代,程序员的核心竞争力:从“编码工匠”到“元问题架构师”的终极进化
  • 小雅
  • 易基因:JEM(IF10.6):单细胞转录组测序(scRNA-seq)揭示过敏性肺部疾病的调控网络|项目文章
  • Services.AddRazorPages解释
  • 2025 年金属线槽厂家最新推荐排行榜:涵盖不锈钢、铝合金、防火等多类型产品,助您精准挑选优质厂家企业
  • 02_通讯录实现
  • 2025 空气离合器生产厂家最新推荐榜:电网冲击缓解技术测评与可靠性排行,含单片多片机型及核心部件企业
  • 2025 气动离合器厂家最新推荐榜权威发布:聚焦博得 PLC 技术与新兴品牌降本优势多片式气动离合器/气动离合器电磁阀/气动离合器气缸/气动离合器摩擦片/单片式气动离合器厂家推荐
  • Unicode 编码解码工具类
  • 2025 木粉源头厂家最新推荐榜:全品类适配 / 稳定供应 / 技术赋能品牌权威解析,采购必看杂/刨花/木塑/化工/造纸/香/猫砂木粉厂家推荐
  • mergeGDS
  • 读书笔记
  • 有奖话题:Data Agent for Meta 能否成为企业级 “数据大脑”?
  • 汉印打印机N41BT驱动 安装后无法打印
  • 新的练习项目
  • 最简单的 Web 打印方案:用 5 分钟上手 web-print-pdf(npm 包) - 实践