redis系列(1)——redis高效的本质:基础键值对的组织和基础数据结构

0. 前言

redis高效存储的本质,是来源它高效的性能设计。这其中,基础的键值对组织方式,是所有设计的基石。首先让我们从基础键值对的组织结构,来了解redis的基础数据类型设计。

1. redis的基础键值对组织

所有的键值对,无论是String、List、Hash、Set、ZSet,都会用一个抽象的通用结构dictEntry放在一个全局hash表里,每个桶单独维护。并通过哈希的方式,去查找每个key。

scss 复制代码
type dictEntry struct{
    key 指针8B (redisObejct16B) 
    value 指针8B(redisObejct16B)
    next 8B
    //最后整个结构会按jemalloc进行一次内存对齐
}

1.1 哈希冲突和rehash

哈希冲突一般通过链式哈希解决,但链太长的时候,查找效率还是很低。这时候会进行rehash,把原本的链分解,扩充hash表的大小,解决长链的问题。

1.1.1 rehash的步骤

  1. redis一开始会有两个hash表,一样的大小,称为A表和B表
  2. B表会扩容到原本的两倍大小
  3. copy A->B
  4. 释放A表

1.1.2 单线程引入的问题------同时使用和扩容

如果整个线程罢工去做rehash,redis单线程会阻塞。为了解决,redis使用了渐进式hash,每处理一个请求时,顺带把这次的rehash做了。

顺这个思路,如果总有些低频的key访问不到,怎么解决?

这时候会有定时任务,定期扫描hash桶,清理这些key

1.1.3 思维导图

2. redis每种类型是怎么设计的?都有哪些底层数据结构?

经过全局hash桶的查询,这时我们定位到了某个具体的key里,接下来让我们看下,每种类型是怎么设计的

每个 Redis 中存储的值,无论哪种类型(字符串、列表、集合等),都有一个对应的 RedisObject,用于描述该值的类型和存储方式。

go 复制代码
// RedisType 表示 Redis 对象的类型。
type RedisType int

const (
    StringType RedisType = iota // 字符串类型
    ListType                    // 列表类型
    SetType                     // 集合类型
    ZSetType                    // 有序集合类型
    HashType                    // 哈希类型
    // 根据需要添加其他类型
)

// EncodingType 表示 Redis 对象的编码类型。
type EncodingType int

const (
    Raw EncodingType = iota  // 原始字符串编码
    Int                      // 整数编码
    EmbStr                   // 短字符串优化编码
    // 根据需要添加其他编码类型
)


// RedisObject 在 Go 中表示一个 Redis 对象。
type RedisObject struct {
    Type     RedisType    // 对象的类型
    Encoding EncodingType // 对象的编码类型
    Ptr      interface{}  // 通用指针,指向实际数据
    LRU      uint64       // 用于 LRU 淘汰策略的最后访问时间
    RefCount int          // 内存管理的引用计数
}

可选的类型有:整数数组、双向链表、哈希表、压缩列表和跳表

2.1 简单动态字符串

简单动态字符串,由两个对象组成:

  1. SDS
  2. RedisObject

2.1.1 SDS内存布局设计------三种编码方式(int、ebmstr、raw)

int编码 保存整型时,RedisObject中,指向实际数据的指针Ptr就直接赋值为整数数据,节约空间

保存字符串时,RedisObject 会有两种编码格式,ebmstr编码和 raw编码

ebmstr编码: 字符串小于等于 44 字节时,RedisObject 中的元数据、指针和 SDS 是一块连续的内存区域,这样就可以避免内存碎片。

raw编码:当字符串大于 44 字节时,SDS 的数据量就开始变多了,Redis 就不再把 SDS 和 RedisObject 布局在一起了,而是会给 SDS 分配独立的空间,并用指针指向 SDS 结构。

scss 复制代码
type SDS struct{
    buf//字节数组 保存实际数据
    Len(4B) //buf 已经使用的长度 
    alloc(4B) //buf的容量,相当于golang的cap
}
go 复制代码
type RedisObject struct{
    元数据 //(8B)
    ptr //(8B) -> SDS
}

2.1.2 实例分析

一个 10 位数的图片 ID 和图片存储对象 ID 都是整数,总共会占用64B。

64B都由哪些部分组成呢?

一共三个部分组成:

    1. key 16B(整数->int编码->16B)
    1. value 16B(整数->int编码->16B)
    1. 全局哈希表dictEntry结构 32B (实际24B,内存对齐后32B)

key、value都是int,所以用int编码,一共16 * 2(key + value) = 32B, 剩下32B,来自全局哈希表。全局hash表结构dicEntry是24B,但是redis的内存对齐,会让他变成2的x次方,所以是32B。

2.2 整数数组

最基础的数组结构,优点是查找快,缺点是插入是O(n)的,不适合经常改动的结构

2.3 压缩列表

压缩列表(ziplist)是一种为节省内存而设计的连续内存数据结构,常用于存储少量数据(如哈希对象、列表对象的早期形态)。它将多个元素紧密排列在一块连续内存中,通过特殊编码方式存储整数和短字符串,从而减少内存碎片。

Golang 复制代码
type ZipList struct{
    Arr []T
    Zlbytes int // 整个列表的长度
    Zltail int // 到列表尾部的偏移量
    Zllen int // 列表中元素的个数
    Zlend // 在列表尾部,表示元素结束
}

2.3.1 压缩列表的整体结构

压缩列表的内存布局如下:

xml 复制代码
<zlbytes> <zltail> <zllen> <entry1> <entry2> ... <entryN> <zlend>

各部分含义:

  1. zlbytes (4 字节):
    整个压缩列表占用的总字节数(包括自身),用于快速定位压缩列表的末尾。
  2. zltail (4 字节):
    压缩列表尾部元素的偏移量(相对于压缩列表起始地址),用于支持从尾部快速访问元素。
  3. zllen (2 字节):
    压缩列表包含的元素数量。若元素数量超过 65535,则该值为 65535,需遍历整个列表获取真实数量。
  4. entry1 ~ entryN
    压缩列表存储的元素,每个元素包含前一个元素长度自身编码实际数据
  5. zlend (1 字节):
    压缩列表的结束标记,固定值为 0xFF(十进制 255)。

2.3.2 压缩列表节点(entry)的结构

每个元素(entry)由三部分组成:

xml 复制代码
<prevlen> <encoding> <data>
  1. prevlen

    前一个元素的长度,用于从后向前遍历列表。

    • 若前一个元素长度小于 254 字节,prevlen 用 1 字节表示;
    • 若前一个元素长度大于等于 254 字节,prevlen 用 5 字节表示(第 1 字节固定为 0xFE,后 4 字节存储实际长度)。
  2. encoding

    元素数据的编码方式,决定了 data 字段的类型和长度。常见编码类型:

    • 字符串编码 :以 000110 开头,后跟字符串长度。
      例如:00xxxxxx 表示长度 ≤ 63 字节的字符串。
    • 整数编码 :以 11 开头,直接存储整数值。
      例如:11000000 表示 4 位有符号整数。
  3. data

    实际存储的数据,格式和长度由 encoding 决定。

2.3.3 整数编码的优化

压缩列表对不同范围的整数采用不同长度的编码:

  • 1 字节:存储范围为 -128 ~ 127 的整数。
  • 2 字节:存储 16 位有符号整数。
  • 5 字节:存储 64 位有符号整数。

例如,整数 5 可能被编码为 11000001(1 字节表示),而非以字符串形式存储(如 0x35),从而节省空间。

2.3.4 连锁更新问题

压缩列表的设计存在一个潜在问题:连锁更新 (Cascade Update)。

当插入或删除一个元素时,可能导致后续元素的 prevlen 字段需要扩展(从 1 字节变为 5 字节),这种扩展可能会传播到后续多个元素,引发连锁更新,影响性能。

Redis 通过限制压缩列表的最大长度和元素大小来缓解问题(如 hash-max-ziplist-entries 配置),如果超过限制,就变成跳表或者哈希表。

2.3.5 应用场景

压缩列表在 Redis 中主要用于:

  • 哈希对象:当哈希键值对数量较少且值为短字符串或小整数时。

  • 列表对象:当列表元素较少且每个元素长度较短时。

  • 有序集合:当成员数量较少且分数为小整数时。

当元素数量或大小超过阈值时,Redis 会将压缩列表转换为更适合大数据量的结构(如哈希表、跳表)。

2.3.6 示例:压缩列表的内存布局

假设一个压缩列表包含三个元素:"a"(字符串)、2(整数)、"bcd"(字符串),其内存布局可能如下:

css 复制代码
[zlbytes=21] [zltail=16] [zllen=3] 
[prevlen=0] [encoding="00000001"] [data="a"] 
[prevlen=3] [encoding="11000010"] [data=2] 
[prevlen=4] [encoding="00000011"] [data="bcd"] 
[zlend=0xFF]

2.3.7 总结

压缩列表通过连续内存布局特殊编码 ,在小数据场景下显著节省内存。其核心优势是内存紧凑,但缺点是插入和删除操作可能引发连锁更新。Redis 通过合理的转换阈值(如 hash-max-ziplist-entries)平衡内存效率和操作性能,使其成为轻量级数据结构的首选。

2.4 跳表

在链表的基础上升级的,维护了多级索引,通过索引位置的几个跳转,实现数据的快速定位。

核心的几个元素:

  1. 基础数据链表结构(数据结点)
  2. 多级索引(索引结点怎么分层、索引-索引结点、索引-数据结点的关联)

2.4.1 增加层级的时机

在插入新节点时会增加层级。具体来说,当为新节点随机生成的层级大于当前跳表的最大层级时,就会增加跳表的层级。

go 复制代码
const (
    maxLevel    = 16
    probability = 0.5
)

// randomLevel 随机生成一个层级
func randomLevel() int {
    level := 1
    // 不断生成随机数进行判断,直到不满足条件或达到最大层级
    for rand.Float64() < probability && level < maxLevel {
        level++
    }
    return level
}

新插入一个值为15的结点,会把x层结点链表,更新到15这个新节点上. 以实现层级扩缩容

2.5 hash表

hash表的基本结构就不多介绍了,核心元素:

  1. key-value
  2. 哈希冲突
  3. rehash问题------渐进式hash(类似上一篇)

3. 总结:Redis 高效存储的核心设计原则

3.1 数据结构自适应切换

  • 小数据用压缩列表、整数编码等轻量级结构,节省内存;大数据自动转为哈希表、跳表,平衡性能。

3.2 单线程与渐进式优化

  • 单线程避免锁竞争,渐进式 Rehash / 迁移无阻塞;定时扫描低频数据保障稳定。

3.3 内存布局优化

  • embstr 编码、内存对齐减少碎片;引用计数与 LRU 优化内存管理。

3.4 核心数据结构适用场景

结构 优势 场景
压缩列表 内存紧凑 小哈希 / 列表 / 有序集合
跳表 有序数据快查 排行榜、范围查询
哈希表 随机读写高效 高频键值对访问
相关推荐
陈阿土i3 小时前
SpringAI 1.0.0 正式版——利用Redis存储会话(ChatMemory)
java·redis·ai·springai
bing_1584 小时前
跨多个微服务使用 Redis 共享数据时,如何管理数据一致性?
redis·微服务·mybatis
多多*5 小时前
微服务网关SpringCloudGateway+SaToken鉴权
linux·开发语言·redis·python·sql·log4j·bootstrap
HAPPY酷6 小时前
Kafka 和Redis 在系统架构中的位置
redis·kafka·系统架构
gaoliheng0067 小时前
Redis看门狗机制
java·数据库·redis
潘yi.8 小时前
Redis哨兵模式
数据库·redis·缓存
努力学习的小廉9 小时前
我爱学算法之—— 前缀和(中)
开发语言·redis·算法
多多*10 小时前
基于rpc框架Dubbo实现的微服务转发实战
java·开发语言·前端·redis·职场和发展·蓝桥杯·safari
MuYiLuck14 小时前
【redis实战篇】第八天
数据库·redis·缓存