Redis数据结构概览

Redis作为高性能的内存数据库,其卓越的性能不仅来自于epoll I/O多路复用技术,更得益于其精心设计的底层数据结构。Redis并没有直接使用传统的数据结构,而是针对内存使用效率和操作性能进行了深度优化。

Redis的五种基本数据类型

Redis提供了五种基本数据类型,每种类型都有其对应的底层数据结构实现:

  1. String(字符串) - SDS(Simple Dynamic String)
  2. List(列表) - 双向链表 + 压缩列表
  3. Hash(哈希) - 压缩列表 + 哈希表
  4. Set(集合) - 整数集合 + 哈希表
  5. Sorted Set(有序集合) - 压缩列表 + 跳跃表

SDS:Redis的字符串实现

传统C字符串的问题

传统的C字符串存在以下问题:

  1. 获取长度需要O(n)时间复杂度:需要遍历整个字符串
  2. 缓冲区溢出:容易造成内存越界
  3. 二进制不安全:不能存储包含’\0’的数据
  4. 频繁内存重分配:每次修改都可能需要重新分配内存

SDS的设计优势

Redis使用SDS(Simple Dynamic String)来解决这些问题:

1
2
3
4
5
struct sdshdr {
int len; // 字符串长度
int free; // 剩余空间
char buf[]; // 字符数组
};

SDS的核心特性

  1. O(1)时间复杂度获取长度:直接读取len字段
  2. 空间预分配:减少内存重分配次数
  3. 惰性空间释放:删除字符时不立即释放内存
  4. 二进制安全:可以存储任意二进制数据
  5. 兼容C字符串:可以直接使用C字符串函数

SDS的空间优化策略

空间预分配

1
2
3
4
5
// 当SDS需要扩展时
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2; // 预分配一倍空间
else
newlen += SDS_MAX_PREALLOC; // 预分配1MB

惰性空间释放

1
2
3
4
5
// 删除字符时,不立即释放内存
void sdstrim(sds s, const char *cset) {
// 只是修改len和free字段,不释放内存
// 下次append时可以复用这些空间
}

跳跃表:有序集合的核心

跳跃表的设计思想

跳跃表是一种概率性数据结构,通过多层链表实现O(log n)的查找性能:

1
2
3
Level 3: 1 ---------> 7 ---------> 19
Level 2: 1 ----> 4 -> 7 ----> 13 -> 19
Level 1: 1 -> 3 -> 4 -> 7 -> 9 -> 13 -> 19 -> 21

Redis跳跃表的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct zskiplistNode {
robj *obj; // 成员对象
double score; // 分值
struct zskiplistNode *backward; // 后退指针
struct zskiplistLevel {
struct zskiplistNode *forward; // 前进指针
unsigned int span; // 跨度
} level[];
} zskiplistNode;

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

跳跃表的优势

  1. 查找效率高:平均O(log n)时间复杂度
  2. 范围查询支持:可以高效进行范围查询
  3. 实现简单:相比红黑树更容易实现和调试
  4. 内存局部性好:链表结构具有良好的内存局部性

压缩列表:内存优化的利器

压缩列表的结构

压缩列表是Redis为了节省内存而设计的一种紧凑型数据结构:

1
<zlbytes> <zltail> <zllen> <entry1> <entry2> ... <entryN> <zlend>
  • zlbytes:整个压缩列表的字节长度
  • zltail:到达表尾节点的偏移量
  • zllen:节点数量
  • entry:各个节点
  • zlend:压缩列表的结束标志

压缩列表节点的结构

1
2
3
4
5
6
7
8
9
typedef struct zlentry {
unsigned int prevrawlensize; // 前一个节点长度字段的大小
unsigned int prevrawlen; // 前一个节点的长度
unsigned int lensize; // 当前节点长度字段的大小
unsigned int len; // 当前节点的长度
unsigned int headersize; // 头部大小
unsigned char encoding; // 编码方式
unsigned char *p; // 指向节点内容的指针
} zlentry;

压缩列表的优势

  1. 内存紧凑:连续存储,减少内存碎片
  2. 缓存友好:连续内存访问,提高缓存命中率
  3. 支持变长数据:可以存储不同长度的数据
  4. 双向遍历:支持从前往后和从后往前的遍历

整数集合:集合的优化实现

整数集合的结构

1
2
3
4
5
typedef struct intset {
uint32_t encoding; // 编码方式
uint32_t length; // 集合包含的元素数量
int8_t contents[]; // 保存元素的数组
} intset;

升级机制

整数集合支持动态升级,当新元素无法用当前编码表示时,会自动升级:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 升级过程
intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
uint8_t curenc = intsetValueEncoding(is->encoding);
uint8_t newenc = intsetValueEncoding(value);
int length = intsetLen(is);

// 设置新编码
is->encoding = newenc;
is = intsetResize(is, length + 1);

// 从后往前重新插入元素
for (int i = length - 1; i >= 0; i--) {
intsetSet(is, i, intsetGetEncoded(is, i, curenc));
}

// 插入新元素
intsetSet(is, length, value);
return is;
}

哈希表:Redis的核心存储

哈希表的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct dictht {
dictEntry **table; // 哈希表数组
unsigned long size; // 哈希表大小
unsigned long sizemask; // 哈希表大小掩码
unsigned long used; // 哈希表已有节点的数量
} dictht;

typedef struct dict {
dictType *type; // 类型特定函数
void *privdata; // 私有数据
dictht ht[2]; // 哈希表
long rehashidx; // rehash索引
unsigned long iterators; // 迭代器数量
} dict;

渐进式rehash

Redis使用渐进式rehash来避免一次性rehash造成的阻塞:

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
int dictRehash(dict *d, int n) {
int empty_visits = n * 10; // 最多访问n*10个空桶

while (n-- && d->ht[0].used != 0) {
dictEntry *de, *nextde;

// 找到非空桶
while (d->ht[0].table[d->rehashidx] == NULL) {
d->rehashidx++;
if (--empty_visits == 0) return 1;
}

// 迁移这个桶中的所有节点
de = d->ht[0].table[d->rehashidx];
while (de) {
nextde = de->next;
// 计算新哈希值
h = dictHashKey(d, de->key) & d->ht[1].sizemask;
de->next = d->ht[1].table[h];
d->ht[1].table[h] = de;
d->ht[0].used--;
d->ht[1].used++;
de = nextde;
}
d->ht[0].table[d->rehashidx] = NULL;
d->rehashidx++;
}

// 检查是否完成rehash
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;
}

数据结构的选择策略

小对象优化

Redis对小对象使用压缩列表进行优化:

  • List:当元素数量 < 512 且每个元素 < 64字节时使用压缩列表
  • Hash:当字段数量 < 512 且每个字段 < 64字节时使用压缩列表
  • Sorted Set:当元素数量 < 128 且每个元素 < 64字节时使用压缩列表

内存使用优化

1
2
3
4
5
6
7
8
// Redis对象的内存布局
typedef struct redisObject {
unsigned type:4; // 对象类型
unsigned encoding:4; // 编码方式
unsigned lru:LRU_BITS; // LRU时间戳
int refcount; // 引用计数
void *ptr; // 指向实际数据的指针
} robj;

性能优化技巧

1. 合理选择数据类型

  • String:适合缓存、计数器
  • Hash:适合存储对象属性
  • List:适合队列、栈
  • Set:适合去重、集合运算
  • Sorted Set:适合排行榜、范围查询

2. 内存优化策略

1
2
3
4
5
# Redis配置优化
maxmemory-policy allkeys-lru # LRU淘汰策略
hash-max-ziplist-entries 512 # Hash压缩列表阈值
hash-max-ziplist-value 64 # Hash压缩列表值阈值
list-max-ziplist-size -2 # List压缩列表阈值

3. 数据结构转换

Redis会根据数据大小自动转换底层数据结构:

1
2
小数据 -> 压缩列表 -> 节省内存
大数据 -> 哈希表/跳跃表 -> 提高性能

实际应用案例

案例1:用户会话存储

1
2
3
4
5
# 使用Hash存储用户会话
hset user:1001 session_id "abc123"
hset user:1001 login_time "2023-01-01 10:00:00"
hset user:1001 last_active "2023-01-01 12:00:00"
hset user:1001 permissions "read,write"

案例2:排行榜实现

1
2
3
4
5
6
7
# 使用Sorted Set实现排行榜
zadd leaderboard 100 "player1"
zadd leaderboard 95 "player2"
zadd leaderboard 90 "player3"

# 获取前10名
zrevrange leaderboard 0 9 withscores

案例3:消息队列

1
2
3
4
5
6
7
# 使用List实现消息队列
lpush message_queue "task1"
lpush message_queue "task2"
lpush message_queue "task3"

# 消费者处理消息
task = rpop message_queue

总结

Redis的底层数据结构设计体现了以下核心思想:

  1. 内存效率优先:通过压缩列表等紧凑结构节省内存
  2. 性能与空间的平衡:根据数据大小动态选择最优结构
  3. 渐进式优化:避免一次性操作造成的性能抖动
  4. 类型特化:针对不同使用场景优化数据结构

理解Redis底层数据结构的实现原理,有助于我们:

  • 更好地选择合适的数据类型
  • 优化内存使用
  • 提高应用性能
  • 避免常见的使用误区