一、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字符串的优势:
-
O(1)获取长度:直接读取len字段,无需遍历
-
二进制安全:不依赖\0结束符,可存储任意二进制数据
-
减少内存重分配:预分配和惰性释放策略
-
兼容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的优势:
-
空间局部性:每个节点包含一个listpack(小数组),利用CPU缓存
-
高效插入删除:链表结构支持O(1)的节点操作
-
高效遍历:listpack支持顺序访问,适合LRANGE操作
-
压缩优化:支持中间节点的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特点:
-
有序存储:contents数组保持有序,二分查找O(logN)
-
自适应编码:根据最大元素值自动选择16/32/64位存储
-
内存紧凑:连续内存分配,无指针开销
六、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的过程:
-
L3: header→50(比40大,下降)
-
L2: header→30(比40小,前进)→50(比40大,下降)
-
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数据结构的核心思想
-
空间时间平衡:在不同场景下选择最合适的编码方式
-
渐进式优化:小数据用紧凑结构,大数据用高效结构
-
CPU缓存友好:连续内存分配,减少缓存失效
-
自动转换:根据数据规模自动升级编码
未来发展趋势
-
更多压缩算法:LZ4、Zstandard等现代压缩算法集成
-
向量化查询:支持AI场景的向量相似度搜索
-
持久化优化:更快的RDB/AOF生成和加载
-
硬件加速:利用CPU新指令集优化性能
学习建议
-
源码阅读路线:
object.c → sds.h → listpack.h → quicklist.h → dict.c -
调试技巧:
# 编译调试版Redis make noopt gdb --args ./src/redis-server # 在关键函数设置断点 (gdb) b createStringObject (gdb) b ziplistPush -
性能测试:
# 使用redis-benchmark测试 redis-benchmark -t set,get -n 1000000 -q # 监控内存使用 redis-cli info memory | grep used_memory_human