Tavio's blog Tavio's blog
首页
  • JVM底层原理
  • 邪恶多线程
  • MyBatis底层原理
  • Spring底层原理
  • MySQL的优化之路
  • ClickHouse的高性能
  • Redis的快速查询
  • RabbitMQ的生产
  • Kafka的高吞吐量
  • ES的入门到入坑
  • MySQL自增ID主键空洞
  • 前端实现长整型排序
  • MySQL无感换表
  • Redis延时双删
  • 高并发秒杀优惠卷
  • AOP无侵入式告警
  • 长短链接跳转
  • 订单超时取消
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Tavio Zhang

努力学习的小码喽
首页
  • JVM底层原理
  • 邪恶多线程
  • MyBatis底层原理
  • Spring底层原理
  • MySQL的优化之路
  • ClickHouse的高性能
  • Redis的快速查询
  • RabbitMQ的生产
  • Kafka的高吞吐量
  • ES的入门到入坑
  • MySQL自增ID主键空洞
  • 前端实现长整型排序
  • MySQL无感换表
  • Redis延时双删
  • 高并发秒杀优惠卷
  • AOP无侵入式告警
  • 长短链接跳转
  • 订单超时取消
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Redis核心数据结构
    • String:二进制安全的动态字符串
      • 1. SDS:解决C字符串的三大缺陷
      • 核心结构体
      • 2. String的三种编码:按需切换的内存优化
      • 实战建议
    • List:从ziplist到quicklist
      • 1. ziplist:紧凑存储的双向链表(小列表)
      • 核心结构
      • entry结构细节
      • 双向遍历
      • 痛点:连锁更新
      • 2. adlist:通用双向链表(旧版大列表)
      • adlist核心特征
      • 3. quicklist:ziplist与链表的融合(Redis3.2+默认)
      • 关键优化
      • 实战建议
    • Hash:键值对集合的两种存储策略
      • 1. ziplist:小Hash的紧凑存储
      • 2. dict:大Hash的高效存储
      • 渐进式rehash:避免主线程阻塞
      • Redis7.0+优化
      • 实战建议
    • Set:无序唯一集合的两种实现
      • 1. intset:纯整数集合的紧凑存储
      • 核心特性
      • 2. dict:通用集合存储
      • 实战建议
    • ZSet:有序唯一集合的跳表+字典组合
      • 1. ziplist:小ZSet的紧凑存储
      • 2. 跳表+dict:大ZSet的高效存储
      • 跳表核心特征
      • 结构示例(简化)
      • 实战建议
    • 总结:Redis数据结构的设计哲学
  • Redis持久化机制
  • Redis高可用架构
  • Redis分布式锁
  • Redis缓存设计
  • Redis大Key与热Key
  • Redis限流
  • Redis IO多路复用
  • Redis过期删除策略
  • Redis Bitmap
  • 《Redis》笔记
Tavio
2023-03-02
目录

Redis核心数据结构

Redis作为高性能键值数据库,其核心优势不仅在于内存存储和单线程模型,更在于对五大基础数据结构(String、List、Hash、Set、ZSet)的极致优化——通过动态切换底层存储结构,在内存占用与读写性能间找到完美平衡。

# String:二进制安全的动态字符串

String是Redis最基础的数据结构,支持存储字符串、数字、二进制数据(如图片、序列化对象),最大容量512MB。其底层并非C语言原生字符串,而是SDS(Simple Dynamic String)——一种专为Redis设计的动态字符串结构,替代了C语言原生字符串,解决了原生字符串的缺陷。

# 1. SDS:解决C字符串的三大缺陷

C语言原生字符串以\0为结束标识,存在三大问题:长度获取需O(n)遍历、二进制数据中含\0会被截断、修改需频繁内存重分配。SDS通过精巧设计彻底解决这些问题:

# 核心结构体

Redis为不同长度字符串设计4种头部(sdshdr8/16/32/64),用最小位数存储长度,避免内存浪费。以最常用的sdshdr8为例:

# 核心代码
struct sdshdr8 {
    uint8_t len;        // 已使用字节数(O(1)获取长度)
    uint8_t alloc;      // 总分配字节数(不含头部和终止符)
    unsigned char flags;// 标识类型(8/16/32/64,占1字节)
    char buf[];         // 柔性数组,存储实际数据(二进制安全)
};
// flags 0=sdshdr8, 1=sdshdr16, 2=sdshdr32,3=sdshdr64
1
2
3
4
5
6
7
8
  • 二进制安全:通过len字段记录有效长度,读取时直接按len截取buf,即使数据含\0也不会截断(末尾额外存\0仅为兼容C函数)。
  • 内存预分配:修改时减少内存重分配次数(malloc/free是昂贵操作):
    • 若新长度≤1MB:alloc翻倍(如从100字节扩容到200字节);
    • 若新长度>1MB:alloc增加1MB(避免过度预分配);
    • 若新长度过大:触发SDS结构体升级(如新数据长度>2^8-1字节,会先创建并分配sdshdr16的结构体,将原sdshdr8的buf数据完整拷贝到sdshdr16的buf中,写入新增数据,更新len字段,释放当前sdshdr8结构体占用的内存,完成扩容)。
  • 惰性缩容:缩短字符串时仅更新len,不释放多余内存,后续写入可复用已分配的buf,减少内存重分配开销。SDS仅升级不会主动降级

# 2. String的三种编码:按需切换的内存优化

String实际是redisObject包裹的底层编码,根据存储内容自动切换:

# 核心代码(redisObject结构)
typedef struct redisObject {
    unsigned type:4;    // 数据类型(STRING/LIST/HASH等)
    unsigned encoding:4;// 编码类型(int/embstr/raw等)
    unsigned lru:24;    // LRU时间戳(用于内存淘汰)
    int refcount;       // 引用计数(内存回收)
    void *ptr;          // 指向底层数据(SDS/整数)
} robj;
1
2
3
4
5
6
7
8
编码类型 适用场景 内存布局 优势
int 字符串为64位内整数(如"123") ptr直接存整数,无SDS 极致省内存,O(1)读写
embstr 短字符串(≤44字节) redisObject + sdshdr + buf连续内存(一次malloc) 内存碎片少,创建/销毁效率高
raw 长字符串(>44字节) redisObject与sdshdr分开分配(两次malloc) 避免长字符串重分配影响对象头
  • 什么是malloc?
    malloc是C语言动态申请堆内存的核心接口,Redis依靠它实现SDS、哈希表等数据结构的动态内存管理,是Redis能灵活存储任意长度的基础,而free是配套的内存回收操作。

  • 为什么embstr上限是44字节?
    redisObject占16字节,sdshdr8头部占3字节,末尾\0占1字节,总固定开销20字节。64字节内存块中剩余44字节用于存储实际数据(64-16-3-1=44),刚好适配CPU缓存行(64字节),提升访问速度。

  • embstr的ptr指向
    ptr字段在embstr编码下,并非指向独立的SDS,而是指向同一块内存中紧跟redisObject后的sdshdr起始地址。

# 实战建议

  • 存储数字时优先用String的int编码(如计数器场景),避免转为embstr/raw;
  • 二进制数据(如图片)建议拆分后存储(单key不超过100KB),避免大字符串扩容开销;
  • 频繁修改的短字符串(如用户昵称)天然适配embstr,无需额外优化。

# List:从ziplist到quicklist

List是有序、可重复的字符串集合,支持两端插入/删除、范围查询,底层实现随数据规模动态变化,核心目标是小数据省内存,大数据保性能。

# 1. ziplist:紧凑存储的双向链表(小列表)

ziplist是Redis为小数据设计的紧凑结构,用连续内存块存储元素 + 无指针开销的设计大幅降低内存占用,替代普通双向链表(普通双向链表每个节点都包含前后指针,内存开销大且易产生碎片)适合节点少、元素小的场景。

# 核心结构

# 结构示意图
+--------+--------+--------+---------+---------+-------+
| zlbytes| zltail | zllen  | entry1  | entry2  | zlend |
+--------+--------+--------+---------+---------+-------+
1
2
3
4
  • zlbytes:总字节数(快速内存重分配);
  • zltail:尾节点偏移量(O(1)定位尾节点);
  • zllen:节点数(≤65535时准确,超则需遍历);
  • entry:存储元素(含prevlen+encoding+data);
  • zlend:结束标记(固定为0xFF)。

# entry结构细节

每个元素由三部分组成:

  • prevlen:前节点长度(1字节或5字节,<254用1字节,否则用1字节0xFE+4字节长度);
  • encoding:数据类型(区分字符串/整数,如0x00表示短字符串);
  • data:实际数据(按encoding规则存储)。

# 双向遍历

  • 正向遍历是从首节点开始,通过解析当前节点的encoding计算长度,定位下一个节点;
  • 反向遍历是通过zltail定位尾节点,再通过节点的prevlen反向找前一个节点。

# 痛点:连锁更新

当某节点长度从253→254时,下节点的prevlen需从1字节扩为5字节,可能触发下下个节点的prevlen更新,形成连锁反应(极端O(n)耗时)。因此Redis对ziplist使用限制严格:

  • 节点数≤list-max-ziplist-entries(默认512);
  • 单个元素≤list-max-ziplist-value(默认64字节)。

# 2. adlist:通用双向链表(旧版大列表)

adlist是Redis早期用于大列表的底层结构,兼顾灵活性但内存开销较高。

# 核心代码(adlist结构)
// 链表节点
typedef struct listNode {
    struct listNode *prev;  // 前驱节点指针
    struct listNode *next;  // 后继节点指针
    void *value;            // 数据指针(支持任意类型)
} listNode;

// 链表本体
typedef struct list {
    listNode *head;          // 头节点指针
    listNode *tail;          // 尾节点指针
    unsigned long len;       // 节点数量(O(1)获取长度)
    void *(*dup)(void *ptr); // 节点值复制函数(自定义逻辑)
    void (*free)(void *ptr); // 节点值释放函数(自定义逻辑)
    int (*match)(void *ptr, void *key); // 节点值匹配函数(自定义逻辑)
} list;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# adlist核心特征

  • 双向遍历+无环设计:通过prev/next指针支持正反遍历,避免死循环;
  • O(1)核心操作:直接通过len字段获取长度,无需遍历;
  • 缺陷:节点分散导致内存碎片多,遍历需频繁切换内存地址,效率低于ziplist。

# 3. quicklist:ziplist与链表的融合(Redis3.2+默认)

为解决ziplist连锁更新和adlist内存碎片问题,quicklist采用“链表包裹ziplist”的设计:每个链表节点(quicklistNode)封装一个ziplist,兼顾紧凑性和灵活性。

# 核心代码(quicklist结构)
// quicklist 整体结构体
typedef struct quicklist {
    quicklistNode *head;          // 链表头节点指针
    quicklistNode *tail;          // 链表尾节点指针
    unsigned long count;          // 所有ziplist中的总元素数(O(1)获取长度)
    unsigned long len;            // quicklistNode的数量(链表节点数)
    int fill : 16;                // 每个ziplist的最大填充限制(元素数/字节数)
    unsigned int compress : 16;   // 压缩深度(控制中间节点LZF压缩)
} quicklist;

// quicklist 链表节点
typedef struct quicklistNode {
    struct quicklistNode *prev;   // 前驱节点指针
    struct quicklistNode *next;   // 后继节点指针
    unsigned char *zl;            // 指向ziplist的指针(未压缩)
    unsigned int zl_bytes;        // ziplist的总字节数
    unsigned int count : 16;      // 当前ziplist中的元素数
    unsigned int encoding : 2;    // 编码方式:RAW(未压缩)/LZF(压缩)
    unsigned int container : 2;   // 容器类型:仅支持ZIPLIST
    unsigned int recompress : 1;  // 标记:是否需要重新压缩(临时解压后)
    unsigned int attempted_compress : 1; // 标记:是否尝试过压缩
    unsigned int extra : 10;      // 预留位
} quicklistNode;

// 压缩后的ziplist结构(LZF压缩)
typedef struct quicklistLZF {
    unsigned int sz;  // 压缩后的字节数
    char compressed[];// 压缩后的ziplist数据
} quicklistLZF;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# 关键优化

  • 压缩策略:通过compress控制中间节点LZF压缩(头尾部不压缩,保证读写效率);
  • 动态拆分/合并:插入元素时若ziplist超fill阈值,自动拆分新节点;删除后若ziplist过空,合并相邻节点。

# 实战建议

  • 消息队列场景建议将list-max-ziplist-entries调大(如1024),减少quicklist节点数;
  • 频繁修改中间元素的场景(如日志记录)避免用List,改用ZSet按时间戳排序;
  • 启用压缩(compress=1)可降低长列表内存占用(如历史记录)。

# Hash:键值对集合的两种存储策略

Hash用于存储field-value键值对(如用户信息:user:1 {name: "xxx", age: 20}),底层根据数据规模在ziplist和dict间切换。

# 1. ziplist:小Hash的紧凑存储

当Hash满足:

  • 节点数≤hash-max-ziplist-entries(默认512);
  • 单个field/value≤hash-max-ziplist-value(默认64字节);

存储格式为field1 → value1 → field2 → value2(连续排列),查找时需遍历ziplist匹配field。

# 2. dict:大Hash的高效存储

dict是Redis实现的高性能哈希表,支持O(1)读写,核心解决普通哈希表扩容阻塞问题。

# 核心代码(dict结构)
// 哈希节点(最小单元)
typedef struct dictEntry {
    void *key;                // 键(Redis中通常是字符串对象)
    union {                   // 值(支持多类型,节省内存)
        void *val;            // 通用值指针(如Hash的value)
        uint64_t u64;         // 无符号整数(优化小整数存储)
        int64_t s64;          // 有符号整数
        double d;             // 浮点数
    } v;
    struct dictEntry *next;   // 下一个节点指针(解决哈希冲突,形成链表)
} dictEntry;

// 哈希表
typedef struct dictht {
    dictEntry **table;        // 哈希槽数组(元素是dictEntry*,初始为NULL)
    unsigned long size;       // 哈希槽数组大小(2的幂次,如4、8、16)
    unsigned long sizemask;   // 掩码 = size - 1,用于计算哈希槽索引(hash & sizemask)
    unsigned long used;       // 已存储的dictEntry数量(键值对总数)
    unsigned long deleted;    // 已标记删除的节点数(惰性删除)
} dictht;

// 字典本体
typedef struct dict {
    dictType *type;           // 自定义操作函数(哈希、对比、复制、释放)
    void *privdata;           // 私有数据(传给自定义函数)
    dictht ht[2];             // 双哈希表:ht[0]正常使用,ht[1]用于rehash
    long rehashidx;           // rehash进度索引(-1表示未开启rehash)
    int16_t pauserehash;      // 标记:是否暂停rehash(如阻塞操作时)
} dict;

// 自定义操作函数(适配不同数据类型)
typedef struct dictType {
    uint64_t (*hashFunction)(const void *key); // 哈希函数(默认SIPHASH)
    int (*keyCompare)(const void *privdata, const void *key1, const void *key2); // 键对比
    void *(*keyDup)(void *privdata, const void *key); // 键复制
    void *(*valDup)(void *privdata, const void *obj); // 值复制
    void (*keyFree)(void *privdata, void *key); // 键释放
    void (*valFree)(void *privdata, void *obj); // 值释放
} dictType;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

# 渐进式rehash:避免主线程阻塞

普通哈希表扩容需一次性迁移所有数据,会阻塞Redis。dict采用分批次迁移:

  1. 触发条件:
    • 扩容:负载因子(used/size)>1(非BGSAVE时)或>5(强制);
    • 缩容:负载因子<0.1或已删除节点数>used/2;
  2. 迁移过程:每次操作(增删改查)或定时任务迁移部分槽,rehashidx记录进度;
  3. 读写逻辑:读查ht[0]→ht[1],写直接入ht[1],保证数据一致性。

# Redis7.0+优化

  • 哈希冲突解决:桶内元素>8时,链表转为跳表(查询从O(n)→O(logn));
  • 异步rehash:迁移任务交给后台线程,彻底避免主线程阻塞。

# 实战建议

  • 存储对象属性时优先用Hash(比多个String节省内存);
  • 高频更新的大Hash(如商品属性)建议提前将hash-max-ziplist-entries设为较小值(如128),避免ziplist遍历耗时;
  • 避免大量小Hash(如千万级用户信息),可按ID分片(user:1000:info、user:2000:info)。

# Set:无序唯一集合的两种实现

Set是无序、唯一的字符串集合(如标签、好友ID),底层根据元素类型自动选择intset或dict。

# 1. intset:纯整数集合的紧凑存储

当Set元素全为整数且数量少(≤set-max-intset-entries,默认512)时,用intset存储:

# 核心代码(intset结构)
typedef struct intset {
    uint32_t encoding; // 编码(INT16/32/64,根据最大元素确定)
    uint32_t length;   // 元素数
    int8_t contents[]; // 柔性数组,存储有序、唯一的整数(按编码对齐)
} intset;
1
2
3
4
5
6

# 核心特性

  • 自动升级:插入元素超出当前编码范围时,扩容内存并迁移所有元素(不可逆);
  • 有序性:元素按升序排列,支持二分查找(O(logn));
  • 唯一性:插入前检查元素是否存在,避免重复。

# 2. dict:通用集合存储

当元素含字符串或数量超阈值时,用dict存储:key为集合元素,value为NULL(仅用key保证唯一性)。

# 实战建议

  • 纯整数集合(如用户ID)优先用Set的intset编码,内存占用仅为List的1/10;
  • 交集/并集操作(SINTER/SUNION)适合用Set,底层通过哈希表高效实现;
  • 避免存储大量重复元素(Set会自动去重,浪费写入资源)。

# ZSet:有序唯一集合的跳表+字典组合

ZSet是按score排序的唯一集合(如排行榜、延迟队列),底层根据数据规模在ziplist和“跳表+dict”间切换。

# 1. ziplist:小ZSet的紧凑存储

当满足:

  • 元素数≤zset-max-ziplist-entries(默认128);
  • 单个元素≤zset-max-ziplist-value(默认64字节);

存储格式为member1 → score1 → member2 → score2(按score升序),插入时需遍历找到位置。

# 2. 跳表+dict:大ZSet的高效存储

为同时支持“按score排序”和“快速查询member的score”,ZSet采用双结构:

  • 跳表(zskiplist):按score排序存储member+score,支持O(logn)插入、删除、范围查询;
  • dict:映射member→score,支持O(1)查询score和判断member是否存在。
# 核心代码(ZSet结构)
// 跳表节点
typedef struct zskiplistNode {
    sds ele;               // member字符串(SDS)
    double score;          // 分值
    struct zskiplistNode *backward; // 后退指针(仅用于尾节点遍历)
    struct zskiplistLevel {
        struct zskiplistNode *forward; // 前进指针
        unsigned long span; // 到下一个节点的元素个数(用于排名计算)
    } level[];             // 随机层数(1-32)
} zskiplistNode;

// 跳表主结构
typedef struct zskiplist {
    struct zskiplistNode *header, *tail; // 头/尾节点
    unsigned long length; // 节点数
    int level;            // 最大层数
} zskiplist;

// ZSet组合结构
typedef struct zset {
    dict *dict; // member → score映射
    zskiplist *zsl; // 按score排序的跳表
} zset;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 跳表核心特征

  • 分层设计:底层(0层)含所有节点,高层节点更少,作为“快速通道”,查找效率O(logn);
  • 严格有序:按score升序排列,所有层的链表遵循同一顺序;
  • 随机层数:新节点层数随机生成(1-32),避免层级结构失衡。

# 结构示例(简化)

假设 Zset 存储 {a:1, b:2, c:3},跳表结构简化如下(仅展示核心层):

# 层 2(最高层):header → c (span=3) → NULL
# 层 1:header → b (span=2) → c (span=1) → NULL
# 层 0(基础层):header → a (span=1) → b (span=1) → c (span=1) → NULL
1
2
3

# 实战建议

  • 排行榜场景用ZADD+ZREVRANGE,利用跳表高效范围查询;
  • 延迟队列(按时间戳score)用ZRANGEBYSCORE获取到期任务;
  • 大ZSet(百万级元素)建议禁用ziplist(调小zset-max-ziplist-entries),避免插入耗时增长。

# 总结:Redis数据结构的设计哲学

Redis对数据结构的优化始终围绕“平衡内存与性能”:

  • 小数据用紧凑结构(ziplist、intset、embstr)减少内存开销;
  • 大数据用高效结构(quicklist、dict、跳表)保证读写性能;
  • 动态切换机制(阈值控制)适配不同场景;
  • 避免阻塞(渐进式rehash、异步操作)保证高可用性。
编辑 (opens new window)
#Redis数据结构
上次更新: 2026/01/21, 19:29:14
Redis持久化机制

Redis持久化机制→

最近更新
01
订单超时取消
01-21
02
双 Token 登录
01-21
03
长短链接跳转
01-21
更多文章>
Theme by Vdoing
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式