Redis7底层数据结构深度解析:从源码透视高性能设计精髓

一、Redis数据结构全景图:从应用层到底层

1. Redis的两层数据结构体系

Redis采用了两层数据结构设计,这种设计模式类似于Java中的接口与实现:

复制代码
// Redis对象结构(server.h 900行)
struct redisObject {
    unsigned type:4;      // 上层数据类型:string、hash、list等
    unsigned encoding:4;  // 底层编码:int、embstr、raw、listpack等
    unsigned lru:LRU_BITS; // LRU/LFU信息
    int refcount;          // 引用计数
    void *ptr;             // 指向实际数据的指针
};

核心字段解读

  • type:用户可见的数据类型,可通过TYPE key命令查看

  • encoding:内部编码方式,可通过OBJECT ENCODING key查看

  • ptr:指向具体数据结构的指针,这才是真正的数据存储位置

2. 查看Redis底层编码的实战技巧

复制代码
# 设置不同类型的数据
127.0.0.1:6379> SET num_key 123
127.0.0.1:6379> SET short_str "hello"
127.0.0.1:6379> SET long_str "这是一个超过44字节的字符串,用于测试raw编码类型..."

# 查看类型和编码
127.0.0.1:6379> TYPE num_key        # string
127.0.0.1:6379> OBJECT ENCODING num_key  # int

127.0.0.1:6379> TYPE short_str      # string  
127.0.0.1:6379> OBJECT ENCODING short_str # embstr

127.0.0.1:6379> TYPE long_str       # string
127.0.0.1:6379> OBJECT ENCODING long_str  # raw

3. Redis 6 vs Redis 7:数据结构演进对比

数据类型 Redis 6 Redis 7 变化说明
String SDS SDS 无变化
List quicklist+ziplist quicklist+listpack ziplist→listpack
Hash hashtable+ziplist hashtable+listpack ziplist→listpack
Set intset+hashtable intset+listpack+hashtable 新增listpack支持
ZSet skiplist+ziplist skiplist+listpack ziplist→listpack

核心演进 :Redis 7用listpack全面替代ziplist,解决了连锁更新问题,提升性能和稳定性。

二、String类型:三种编码的智能选择

1. String的三种编码方式

Redis会根据字符串的内容和长度,智能选择最合适的编码方式:

复制代码
// 源码位置:object.c 614行,tryObjectEncodingEx方法
robj *tryObjectEncodingEx(robj *o, int try_trim) {
    // 检查是否为整数
    if (o->encoding == OBJ_ENCODING_RAW && sdslen(o->ptr) <= 20) {
        long long value;
        if (string2ll(o->ptr,sdslen(o->ptr),&value)) {
            // 如果是整数,使用int编码
            if (value >= 0 && value < OBJ_SHARED_INTEGERS) {
                // 小整数共享对象池(0-999)
                decrRefCount(o);
                return shared.integers[value];
            }
            // 普通整数编码
            o->encoding = OBJ_ENCODING_INT;
            sdsfree(o->ptr);
            o->ptr = (void*) value;
            return o;
        }
    }
    // 如果字符串长度 <= 44,使用embstr
    if (sdslen(o->ptr) <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT) {
        return createEmbeddedStringObject(o->ptr,sdslen(o->ptr));
    }
    // 否则使用raw编码
    return o;
}

2. 编码规则详解

编码 条件 内存布局 适用场景
int 值可转为long型整数,且数字长度≤20 ptr直接存储整数值 计数器、ID等数值类型
embstr 字符串长度≤44字节 RedisObject和SDS连续内存分配 短字符串、JSON片段
raw 字符串长度>44字节 RedisObject和SDS分开分配 长文本、大JSON

为什么是44字节?

复制代码
// server.h 第73行
#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44

44字节是经过精心计算的平衡点,考虑了内存对齐和CPU缓存行大小(通常64字节),确保embstr能在单个缓存行中容纳。

3. SDS(简单动态字符串):Redis的字符串引擎

复制代码
// sds.h 45行,SDS结构定义
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len;        // 已使用长度
    uint8_t alloc;      // 总分配长度
    unsigned char flags;// 类型标志
    char buf[];         // 字符数组
};

SDS相比C字符串的优势

  1. O(1)获取长度:直接读取len字段,无需遍历

  2. 二进制安全:不依赖\0结束符,可存储任意二进制数据

  3. 减少内存重分配:预分配和惰性释放策略

  4. 兼容C函数:buf[]以\0结尾,兼容部分C字符串函数

三、Hash类型:listpack与hashtable的智能切换

1. 编码转换规则

Hash类型根据数据规模和配置参数,在listpack和hashtable之间自动切换:

复制代码
# 查看Hash相关配置
127.0.0.1:6379> CONFIG GET hash*
1) "hash-max-listpack-entries"
2) "512"
3) "hash-max-listpack-value"
4) "64"

# 实验:观察编码转换
127.0.0.1:6379> HSET user:1 id 1 name "Tom"
(integer) 2
127.0.0.1:6379> OBJECT ENCODING user:1  # listpack

127.0.0.1:6379> CONFIG SET hash-max-listpack-entries 3
OK
127.0.0.1:6379> HSET user:1 email "tom@example.com"
127.0.0.1:6379> OBJECT ENCODING user:1  # hashtable

转换阈值

  • listpack:字段数 ≤ 512 且 所有值长度 ≤ 64字节

  • hashtable:超过任一阈值时转换

2. listpack:ziplist的终极进化

ziplist的致命缺陷:连锁更新

复制代码
// 连锁更新示例
[entry1(253字节)][entry2(253字节)]...[entryN(253字节)]

当在头部插入一个254字节的新元素时,entry1的previous_entry_length需要从1字节扩展到5字节,导致entry1总长度变为257字节,进而引发entry2、entry3...的连锁更新。

listpack的解决方案

复制代码
// listpack.h 49行
typedef struct listpackEntry {
    unsigned char *sval;
    uint32_t slen;
    long long lval;
} listpackEntry;

listpack的每个entry只记录自身长度,消除了前向依赖,彻底解决了连锁更新问题。

3. Hash的底层数据结构

复制代码
// dict.h 84行,字典结构
struct dict {
    dictType *type;          // 类型特定函数
    dictEntry **ht_table[2]; // 哈希表数组
    unsigned long ht_used[2];// 已使用节点数
    long rehashidx;          // 重哈希索引
    // ... 其他字段
};

// dict.c 63行,哈希表节点
struct dictEntry {
    void *key;               // 键
    union {
        void *val;           // 值
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;  // 下个节点(拉链法)
};

四、List类型:quicklist与listpack的完美结合

1. 编码策略

复制代码
# 默认配置
127.0.0.1:6379> CONFIG GET list*
1) "list-max-listpack-size"
2) "-2"  # 每个节点最大8KB
3) "list-compress-depth"
4) "0"   # 压缩深度

# 编码实验
127.0.0.1:6379> LPUSH list1 "a"
127.0.0.1:6379> OBJECT ENCODING list1  # listpack

127.0.0.1:6379> CONFIG SET list-max-listpack-size 2
127.0.0.1:6379> LPUSH list2 a b c d e
127.0.0.1:6379> OBJECT ENCODING list2  # quicklist

2. quicklist:数组与链表的融合

quicklist结构设计

复制代码
// quicklist.h 36行
typedef struct quicklist {
    quicklistNode *head;     // 头节点
    quicklistNode *tail;     // 尾节点
    unsigned long count;     // 元素总数
    unsigned long len;       // 节点数
    // ... 其他字段
} quicklist;

typedef struct quicklistNode {
    struct quicklistNode *prev;  // 前驱
    struct quicklistNode *next;  // 后继
    unsigned char *entry;        // 指向listpack
    size_t sz;                   // listpack大小
    unsigned int count : 16;     // 元素数量
    // ... 其他字段
} quicklistNode;

quicklist的优势

  1. 空间局部性:每个节点包含一个listpack(小数组),利用CPU缓存

  2. 高效插入删除:链表结构支持O(1)的节点操作

  3. 高效遍历:listpack支持顺序访问,适合LRANGE操作

  4. 压缩优化:支持中间节点的LZF压缩

3. 压缩深度配置

复制代码
# 压缩深度配置说明
# 0: 关闭压缩
# 1: 不压缩头尾节点,压缩中间节点
# 2: 不压缩头2个和尾2个节点
list-compress-depth 1

压缩示意图:

复制代码
[head(uncompressed)] -> [node1(compressed)] -> ... -> [tail(uncompressed)]

五、Set类型:三种编码的智能选择

1. 编码转换规则

Set类型根据元素类型和数量,选择最合适的编码:

复制代码
# 整数集合
127.0.0.1:6379> SADD set1 1 2 3 4 5
127.0.0.1:6379> OBJECT ENCODING set1  # intset

# 短字符串集合(listpack)
127.0.0.1:6379> SADD set2 a b c d
127.0.0.1:6379> OBJECT ENCODING set2  # listpack

# 大集合或长字符串(hashtable)
127.0.0.1:6379> CONFIG SET set-max-listpack-entries 3
127.0.0.1:6379> SADD set3 a b c d e
127.0.0.1:6379> OBJECT ENCODING set3  # hashtable

配置参数

  • set-max-intset-entries 512:intset最大元素数

  • set-max-listpack-entries 128:listpack最大元素数

  • set-max-listpack-value 64:listpack最大元素大小

2. intset:纯整数集合的极致优化

复制代码
// intset.h 35行
typedef struct intset {
    uint32_t encoding;  // 编码方式:16/32/64位
    uint32_t length;    // 元素数量
    int8_t contents[];  // 元素数组
} intset;

intset特点

  1. 有序存储:contents数组保持有序,二分查找O(logN)

  2. 自适应编码:根据最大元素值自动选择16/32/64位存储

  3. 内存紧凑:连续内存分配,无指针开销

六、ZSet类型:skiplist与listpack的协同作战

1. 编码选择策略

ZSet根据数据规模和配置,在listpack和skiplist之间切换:

复制代码
# 配置参数
127.0.0.1:6379> CONFIG GET zset*
1) "zset-max-listpack-entries"
2) "128"
3) "zset-max-listpack-value"
4) "64"

# 实验:小数据用listpack,大数据用skiplist
127.0.0.1:6379> ZADD zset1 1.0 "a" 2.0 "b"
127.0.0.1:6379> OBJECT ENCODING zset1  # listpack

127.0.0.1:6379> CONFIG SET zset-max-listpack-entries 2
127.0.0.1:6379> ZADD zset2 1.0 "a" 2.0 "b" 3.0 "c"
127.0.0.1:6379> OBJECT ENCODING zset2  # skiplist

2. skiplist:跳表的数据结构设计

复制代码
// server.h 中skiplist相关定义
typedef struct zskiplistNode {
    sds ele;                    // 元素值
    double score;               // 分数
    struct zskiplistNode *backward; // 后退指针
    struct zskiplistLevel {
        struct zskiplistNode *forward; // 前进指针
        unsigned long span;      // 跨度
    } level[];                  // 层数组
} zskiplistNode;

typedef struct zskiplist {
    struct zskiplistNode *header, *tail;
    unsigned long length;       // 节点数量
    int level;                  // 最大层数
} zskiplist;

skiplist的时间复杂度

操作 时间复杂度 说明
插入 O(logN) 平均情况
删除 O(logN) 平均情况
查找 O(logN) 平均情况
范围查询 O(logN + M) M为范围内元素数

3. 跳表示例:查找过程演示

复制代码
Level 3:  header ----------------------------> 50
Level 2:  header ------------> 30 -----------> 50
Level 1:  header ----> 10 ----> 30 ----> 40 --> 50
Level 0:  header -> 5 -> 10 -> 20 -> 30 -> 40 -> 50

查找元素40的过程:

  1. L3: header→50(比40大,下降)

  2. L2: header→30(比40小,前进)→50(比40大,下降)

  3. L1: 30→40(找到)

七、Redis性能优化深度解析

1. Redis为什么这么快?

多维度优化策略

优化维度 具体实现 性能提升
内存访问 CPU缓存友好数据结构 10-100倍
数据结构 自适应编码选择 3-10倍
算法优化 渐进式Rehash、惰性删除 2-5倍
网络模型 单线程+IO多路复用 避免锁竞争
协议设计 RESP简单高效 减少解析开销

2. 内存优化技巧

复制代码
// 示例:小整数共享池
#define OBJ_SHARED_INTEGERS 1000
robj *shared_integers[OBJ_SHARED_INTEGERS];

// 创建小整数对象时直接复用
if (value >= 0 && value < OBJ_SHARED_INTEGERS) {
    decrRefCount(o);
    return shared_integers[value];
}

3. 渐进式Rehash机制

复制代码
// dict.c 中的rehash过程
int dictRehash(dict *d, int n) {
    // 每次只迁移n个桶,避免长时间阻塞
    for (int i = 0; i < n; i++) {
        // 迁移一个桶
        if (d->ht[0].used == 0) {
            // 迁移完成
            zfree(d->ht[0].table);
            d->ht[0] = d->ht[1];
            _dictReset(&d->ht[1]);
            d->rehashidx = -1;
            return 0;
        }
    }
    return 1;
}

八、面试高频问题解析

1. Redis 6 vs Redis 7的主要区别?

数据结构层面

  • listpack全面替代ziplist,解决连锁更新问题

  • stream类型底层改用listpack存储

  • 新增更多配置参数支持精细化控制

性能层面

  • 内存使用更稳定,避免连锁更新导致的性能抖动

  • 小数据集合性能更优

2. 连锁更新问题详解

复制代码
# 连锁更新模拟
def chain_update():
    # 假设每个entry 250字节
    entries = [250] * 100  # 100个250字节的entry
    
    # 在头部插入一个260字节的新entry
    new_entry = 260
    entries.insert(0, new_entry)
    
    # 第一个entry的previous_entry_length需要扩展
    entries[1] += 4  # 从1字节扩展到5字节
    
    # 现在第一个entry长度变为254,触发第二个entry扩展
    for i in range(1, len(entries)):
        if entries[i] >= 254:
            entries[i+1] += 4  # 继续扩展

3. 如何根据业务选择合适的数据类型?

决策矩阵

业务场景 推荐类型 底层编码 理由
计数器 String int O(1)操作,内存最小
对象缓存 Hash listpack 字段局部性,支持部分获取
消息队列 List quicklist 高效插入删除,支持范围查询
好友关系 Set intset/listpack 去重,集合运算
排行榜 ZSet skiplist 有序,范围查询高效

九、性能调优实战指南

1. 监控数据结构使用情况

复制代码
# 使用redis-rdb-tools分析RDB文件
rdb -c memory dump.rdb --bytes 128 --largest 10

# 输出示例
database,type,key,size_in_bytes,encoding,num_elements,len_largest_element
0,string,user:1001,96,embstr,1,16
0,hash,product:2001,2048,listpack,25,32
0,zset,leaderboard,10240,skiplist,1000,8

2. 配置优化建议

复制代码
# redis.conf 优化配置

# Hash类型:根据业务调整
hash-max-listpack-entries 1000    # 适当增大,减少hashtable转换
hash-max-listpack-value 100       # 根据值大小调整

# List类型:平衡内存和性能
list-max-listpack-size -2         # 8KB节点大小
list-compress-depth 1             # 压缩中间节点

# ZSet类型:根据数据规模
zset-max-listpack-entries 256     # 适当增大
zset-max-listpack-value 128       # 根据值大小调整

# 内存淘汰策略
maxmemory-policy volatile-lru     # 推荐使用
maxmemory 4gb                     # 设置最大内存

3. Java客户端最佳实践

复制代码
// 使用连接池和Pipeline优化
public class RedisOptimization {
    
    private JedisPool jedisPool;
    
    // 批量操作使用Pipeline
    public List<Object> batchGet(List<String> keys) {
        try (Jedis jedis = jedisPool.getResource()) {
            Pipeline pipeline = jedis.pipelined();
            for (String key : keys) {
                pipeline.get(key);
            }
            return pipeline.syncAndReturnAll();
        }
    }
    
    // 使用合适的数据结构
    public void storeUserInfo(String userId, User user) {
        // 反例:使用多个String存储
        // jedis.set("user:"+userId+":name", user.getName());
        // jedis.set("user:"+userId+":age", String.valueOf(user.getAge()));
        
        // 正例:使用Hash存储
        Map<String, String> hash = new HashMap<>();
        hash.put("name", user.getName());
        hash.put("age", String.valueOf(user.getAge()));
        hash.put("email", user.getEmail());
        jedis.hmset("user:" + userId, hash);
        
        // 设置过期时间
        jedis.expire("user:" + userId, 3600);
    }
}

十、总结与展望

Redis数据结构的核心思想

  1. 空间时间平衡:在不同场景下选择最合适的编码方式

  2. 渐进式优化:小数据用紧凑结构,大数据用高效结构

  3. CPU缓存友好:连续内存分配,减少缓存失效

  4. 自动转换:根据数据规模自动升级编码

未来发展趋势

  1. 更多压缩算法:LZ4、Zstandard等现代压缩算法集成

  2. 向量化查询:支持AI场景的向量相似度搜索

  3. 持久化优化:更快的RDB/AOF生成和加载

  4. 硬件加速:利用CPU新指令集优化性能

学习建议

  1. 源码阅读路线

    复制代码
    object.c → sds.h → listpack.h → quicklist.h → dict.c
  2. 调试技巧

    复制代码
    # 编译调试版Redis
    make noopt
    gdb --args ./src/redis-server
    
    # 在关键函数设置断点
    (gdb) b createStringObject
    (gdb) b ziplistPush
  3. 性能测试

    复制代码
    # 使用redis-benchmark测试
    redis-benchmark -t set,get -n 1000000 -q
    
    # 监控内存使用
    redis-cli info memory | grep used_memory_human
相关推荐
杭州杭州杭州2 小时前
数据结构与算法(5)---二叉树
数据结构
万象.2 小时前
redis数据结构list的基本指令
数据结构·redis·list
zephyr052 小时前
C++ STL unordered_set 与 unordered_map 完全指南
开发语言·数据结构·c++
難釋懷2 小时前
Redis桌面客户端
数据库·redis·缓存
填满你的记忆2 小时前
【从零开始——Redis 进化日志|Day5】分布式锁演进史:从 SETNX 到 Redisson 的完美蜕变
java·数据库·redis·分布式·缓存
hanqunfeng2 小时前
(四十)SpringBoot 集成 Redis
spring boot·redis·后端
難釋懷3 小时前
Jedis快速入门
redis·缓存
漫随流水3 小时前
leetcode算法(112.路径总和)
数据结构·算法·leetcode·二叉树
小北方城市网3 小时前
SpringBoot 集成 MinIO 实战(对象存储):实现高效文件管理
java·spring boot·redis·分布式·后端·python·缓存