Redis数据编码详解

 更新时间:2026年03月20日 15:34:26   作者:czlczl20020925  
这篇文章主要介绍了Redis数据编码的相关知识,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
struct redisObject {
    unsigned type:4;       // [0-3 bit] 对象类型 (如 String)
    unsigned encoding:4;   // [4-7 bit] 编码方式 (如 int/embstr/raw)
    unsigned lru:24;       // [8-31 bit] 缓存淘汰数据
    int refcount;          // [32-63 bit] 引用计数 (4字节)
    void *ptr;             // [64-127 bit] 关键指针 (8字节)
};

String

在 Redis 的底层实现中,String(字符串) 类型并不只有一种形态。为了平衡“内存占用”与“处理性能”,Redis 会根据字符串的内容和长度,在 intembstrraw 三种编码方式之间自动切换。

这三种编码都封装在 redisObject 这个“外壳”下,通过 encoding 字段进行区分。

struct sdshdr8 {
    uint8_t len;    /* 已使用长度 */
    uint8_t alloc;  /* 总分配空间(不含头和 \0) */
    unsigned char flags; /* 类型标志(如 sdshdr8, sdshdr16 等) */
    char buf[];     /* 实际字节数组 */
};

1.int编码:直接存储整数

当一个字符串对象保存的是整数值,且这个整数可以用 long 类型(8 字节有符号整数)表示时,Redis 就会使用 int 编码。

  • 物理特征:它不会分配额外的 SDS 空间,而是直接将整数值存储在 redisObject 结构体的 ptr 指针字段中(通过强制类型转换)。
  • 共享对象优化:Redis 启动时会预先创建 0 ∼ 9999 0 \sim 9999 09999 这 10,000 个整数对象。如果你存的值在这个范围内,所有的 Key 都会指向同一个物理内存地址,引用计数加 1,内存开销几乎为零。
  • 适用场景:计数器、ID 存储等数值场景。

2.embstr编码:嵌入式短字符串

当字符串的长度 小于等于 44 字节 时,Redis 使用 embstr 编码。这是为了极致压榨小对象的性能。

  • 物理特征redisObjectsdshdr(SDS 头部及数据)在内存中是连续的一整块。它是通过一次 malloc 申请出来的。
  • 核心逻辑
    • 只读性:它是只读的,任何修改操作(如 APPEND)都会迫使它先升级为 raw
    • 高性能:由于内存连续,CPU 缓存命中率极高,且分配/释放内存只需要一次系统调用。
  • 计算门槛:44 字节的限制是为了让整个对象(16B redisObject + 3B sdshdr8 + 1B \0 + 44B Data)刚好适配内存分配器的 64 字节 内存槽位。

3.raw编码:常规长字符串

当字符串的长度 大于 44 字节,或者对 embstr 进行了修改操作时,Redis 会使用 raw 编码。

  • 物理特征redisObjectsdshdr 分布在两块不连续的内存空间中。ptr 指针指向独立的 SDS 区域。
  • 核心逻辑
    • 可扩展性:适合存储长文本、二进制数据或频繁修改的字符串。
    • 分配代价:创建或销毁对象需要两次 mallocfree
  • 适用场景:JSON 数据、序列化后的对象、较大的文本内容。

List

Redis3.2之前:ZipList/LinkedList

在 Redis 3.2 之前,List 的实现非常简单粗暴:当数据量小时使用 ZipList(压缩列表),通过连续内存压榨空间;当数据量大或字符串长时,直接转换为 LinkedList(双向链表),通过指针实现灵活增删,但代价是每个节点都要背负两个 8 字节指针的沉重负担,且内存碎片极多。

Redis3.2之后:QuickList

RedisObject中的*ptr指向quicklist对象

typedef struct quicklist {
    quicklistNode *head;      /* 指向头节点 */
    quicklistNode *tail;      /* 指向尾节点 */
    unsigned long count;      /* 所有元素总数 */
    unsigned long len;        /* 节点(车厢)总数 */
    int fill : 16;            /* 节点填充因子 */
    unsigned int compress : 16; /* 压缩深度 */
} quicklist;
typedef struct quicklistNode {
    struct quicklistNode *prev; /* 前驱指针 */
    struct quicklistNode *next; /* 后继指针 */
    unsigned char *zl;          /* 指向物理内存中的连续块 (ZipList/Listpack) */
    unsigned int sz;            /* 连续块占用的总字节数 */
    unsigned int count : 16;    /* 连续块包含的元素个数 */
    // ... 其他标志位
} quicklistNode;

Set

Redis 的 Set(集合) 编码设计同样遵循“从小到大”的进化逻辑。它在物理实现上主要在 IntSet(整数集合)Listpack(紧凑列表,Redis 7.2+)Hashtable(哈希表) 之间切换。

它的核心哲学是:如果全是小整数,我用数组排好序;如果有字符串,我用哈希表锁死。

1. 物理结构:intset(整数集合)

当集合满足以下 两个条件 时,Redis 优先使用 intset

  1. 集合内所有成员均为 整数
  2. 成员数量小于配置参数 set-max-intset-entries(默认 512 个)。

内存布局与查找逻辑

intset 是一块绝对连续的内存空间。

  • 物理存储:内部是一个有序数组,支持 int16_tint32_tint64_t 编码。
  • 有序性:元素在数组内按从小到大严格排序。
  • 查找算法:使用 二分查找(Binary Search),时间复杂度为 O ( log ⁡ N ) O(\log N) O(logN)
  • 升级逻辑:当新插入的整数超出当前位宽(如 int16 存入 int32)时,会触发整块内存的重新分配和数据迁移。注意,为了保持效率,该过程不可逆(不支持降级)

2. 物理结构:listpack(紧凑列表)

这是 Redis 7.2 引入的新物理层。在旧版本中,集合只要出现一个字符串就会立刻膨胀为 dict,而 listpack 充当了中间的缓冲带。

  • 触发场景:集合中包含字符串,但成员数量和单个字符串长度未达到 set-max-listpack-entriesset-max-listpack-value 阈值。
  • 物理特征:连续字节流存储。
  • 性能权衡:虽然查找复杂度退化为 O ( N ) O(N) O(N)(顺序遍历),但由于数据规模极小,其内存利用率远高于 dict,且在小数据量下,连续内存对 CPU 缓存的友好性抵消了 O ( N ) O(N) O(N) 的算法劣势。

3. 物理结构:dict(字典 / 逻辑名称 HashTable)

当集合规模超过阈值,或包含长字符串时,Redis 会使用 dict 作为终极物理载体。

物理映射与内存布局

此时 redisObject->ptr 指向一个真实的 dict 结构体实例。

  • Key (键):存储集合的成员,指向一个 SDS 字符串对象。
  • Value (值):物理上统一设置为 NULL 指针。
  • 唯一性保证:直接利用 dict 自身的哈希碰撞处理和 Key 唯一性逻辑实现集合去重。
  • 性能特征:查找复杂度为 O ( 1 ) O(1) O(1)。支持渐进式 Rehash,在数据量极大时仍能保持稳定的响应速度。

4. 宏观物理映射:RedisObject 的指向

对于 Set 来说,redisObject 的包装方式非常直观:

字段IntSet 编码Hashtable 编码
typeOBJ_SETOBJ_SET
encodingOBJ_ENCODING_INTSETOBJ_ENCODING_HT
ptr 指向一整块连续的 intset 结构一个复杂的 dict 字典结构

ZSet

Redis 的 ZSet(有序集合) 在底层编码上设计得最为复杂,因为它必须同时满足 O ( 1 ) O(1) O(1) 成员查分 O ( log ⁡ N ) O(\log N) O(logN) 按分数排序/范围检索 这两个核心需求。

其物理实现主要分为两个阶段:listpackdict + zskiplist

1. 紧凑阶段:listpack(紧凑列表)

当 ZSet 满足以下两个条件时,Redis 使用 listpack 编码(OBJ_ENCODING_LISTPACK):

  1. 成员数量小于 zset-max-listpack-entries(默认 128)。
  2. 所有成员字符串长度小于 zset-max-listpack-value(默认 64 字节)。

物理存储逻辑

listpack 内部,成员(Member)和分值(Score)被存储为两个相邻的 Entry

  • 布局[Member1, Score1, Member2, Score2, ...]
  • 有序性:内部元素按分值(Score)从小到大严格排序
  • 性能特征:由于是连续内存,插入和查找涉及 O ( N ) O(N) O(N) 的顺序遍历及内存搬迁。但在小数据量下,这种结构的 CPU 缓存命中率极高,且省去了复杂的指针开销。

2. 进化阶段:zset结构体 (跳表 + 字典)

当数据量突破阈值后,redisObject->ptr 会指向一个专门的 zset 结构体。这是一个双重物理结构的组合:

typedef struct zset {
    dict *dict;          /* 成员 -> 分值的哈希表 */
    zskiplist *zsl;      /* 按分数排序的跳跃表 */
} zset;

A. 物理组件一:dict(字典)

  • 作用:实现 O ( 1 ) O(1) O(1) 复杂度的 ZSCORE 操作。
  • 逻辑:Key 是成员(SDS),Value 是分值(double)。
  • 必要性:如果没有 dict,查找一个成员的分数需要遍历跳表,复杂度为 O ( log ⁡ N ) O(\log N) O(logN)

B. 物理组件二:zskiplist(跳跃表)

  • 作用:实现高效的范围查询(ZRANGE)和排名计算(ZRANK)。
  • 逻辑:节点按分数排序。每个节点包含多个层级的指针,支持快速跳跃寻址。
  • 性能:平均查找复杂度为 O ( log ⁡ N ) O(\log N) O(logN)

3. 内存优化:SDS 的“引用共享”

你可能会担心:同一个成员既存在 dict 里,又存在 zskiplist 里,岂不是浪费了一倍内存?

物理真相
dict 的 Key 和 zskiplistNodeele 指向的是同一个物理内存地址(同一个 SDS 对象)。

  • Redis 只是在两个数据结构中各存了一个 指针
  • 这种设计通过增加少量指针开销(每个节点约几十字节),换取了两个维度的极致查询速度。

4. 物理特性对比表

物理结构逻辑编码 (Encoding)核心优势算法复杂度内存特征
listpackLISTPACK极致节省内存O ( N ) O(N) O(N) (查找/插入)连续内存,无碎片
zset (复合)SKIPLIST全能性能查分 O ( 1 ) O(1) O(1),范围 O ( log ⁡ N ) O(\log N) O(logN)双重索引,指针较多

5. 状态转换逻辑

ZSet 的转换通常是单向不可逆的:

  • 一旦数据量超过阈值,listpack 会被拆解,重新装载进一个新的 dictzskiplist 中。
  • 原因:从复杂的双重结构回退到连续内存块涉及大规模的内存重分配和 CPU 计算,收益不抵成本。

Hash

Redis 的 Hash(哈希) 结构在底层编码的设计上,逻辑与 ZSet 非常相似:在数据量小时采用紧凑的连续内存,在数据量大时进化为散列表。

目前的物理实现主要分为 listpackdict 两种。

1. 紧凑编码:listpack(紧凑列表)

当 Hash 结构满足以下两个条件时,Redis 使用 listpack 存储(编码名称为 OBJ_ENCODING_LISTPACK):

  1. 哈希中字段(Field)的数量小于 hash-max-listpack-entries(默认 512 个)。
  2. 所有字段名和值的长度都小于 hash-max-listpack-value(默认 64 字节)。

物理存储逻辑

listpack 的字节流中,Field 和 Value 是作为两个相邻的 Entry 存储的:

  • 布局[Field1, Value1, Field2, Value2, ...]
  • 查找方式:完全依靠顺序遍历。由于内存是绝对连续的,CPU 在读取时可以利用预取机制(Prefetching),在小规模数据下速度极快。
  • 内存优势:没有指针开销,没有内存对齐的空隙,空间利用率达到极致。

2. 散列编码:dict(字典)

一旦数据量突破阈值,或者某个 Value 太长,Redis 就会将物理结构转换为 dict(编码名称为 OBJ_ENCODING_HT)。

物理实现逻辑

此时 redisObject->ptr 指向一个真实的 dict 结构体:

  • Key (键):存储的是 Hash 的字段名(Field),物理上是一个 SDS 对象。
  • Value (值):存储的是 Hash 的字段值(Value),物理上同样是一个 SDS 对象。
  • 冲突处理:使用拉链法(链地址法)解决哈希冲突。
  • 性能特征:查找、插入和删除的复杂度均为 O ( 1 ) O(1) O(1)

到此这篇关于Redis数据编码详解的文章就介绍到这了,更多相关redis数据编码内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 一文搞懂阿里云服务器部署Redis并整合Spring Boot

    一文搞懂阿里云服务器部署Redis并整合Spring Boot

    这篇文章主要介绍了一文搞懂阿里云服务器部署Redis并整合Spring Boot,文章围绕主题展开详细的内容介绍,具有一定的参考价值,需要的小伙伴可以参考一下
    2022-09-09
  • 让Redis在你的系统中发挥更大作用的几点建议

    让Redis在你的系统中发挥更大作用的几点建议

    Redis在很多方面与其他数据库解决方案不同:它使用内存提供主存储支持,而仅使用硬盘做持久性的存储;它的数据模型非常独特,用的是单线程。另一个大区别在于,你可以在开发环境中使用Redis的功能,但却不需要转到Redis
    2014-06-06
  • 关于Redis你可能不了解的一些事

    关于Redis你可能不了解的一些事

    这篇文章主要给大家介绍了关于Redis你可能不了解的一些事,对大家学习或者使用Redis具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2019-04-04
  • 如何利用 Redis 实现接口频次限制

    如何利用 Redis 实现接口频次限制

    这篇文章主要介绍了如何利用 Redis 实现接口频次限制,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-02-02
  • Redis集群水平扩展、集群中添加以及删除节点的操作

    Redis集群水平扩展、集群中添加以及删除节点的操作

    这篇文章主要介绍了Redis集群水平扩展、集群中添加以及删除节点的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-03-03
  • Redis+Caffeine实现多级缓存的步骤

    Redis+Caffeine实现多级缓存的步骤

    随着不断的发展,这一架构也产生了改进,在一些场景下可能单纯使用Redis类的远程缓存已经不够了,还需要进一步配合本地缓存使用,例如Guava cache或Caffeine,从而再次提升程序的响应速度与服务性能,这篇文章主要介绍了Redis+Caffeine实现多级缓存,需要的朋友可以参考下
    2024-01-01
  • Redis持久化深入详解

    Redis持久化深入详解

    这篇文章主要介绍了Redis持久化深入详解,讲解的还是比较详细的,有感兴趣的同学可以学习下
    2021-03-03
  • 分布式使用Redis实现数据库对象自增主键ID

    分布式使用Redis实现数据库对象自增主键ID

    本文介绍在分布式项目中使用Redis生成对象的自增主键ID,通过Redis的INCR等命令实现计数器功能,具有一定的参考价值,感兴趣的可以了解一下
    2024-12-12
  • 关于Redis未授权访问的问题

    关于Redis未授权访问的问题

    这篇文章主要介绍了Redis未授权访问的问题,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-07-07
  • Redis动态字符串SDS的实现

    Redis动态字符串SDS的实现

    SDS在Redis中是实现字符串对象的工具,本文主要介绍了Redis动态字符串SDS的实现,具有一定的参考价值,感兴趣的可以了解一下
    2023-11-11

最新评论