Redis数据结构概览
Redis作为高性能的内存数据库,其卓越的性能不仅来自于epoll I/O多路复用技术,更得益于其精心设计的底层数据结构。Redis并没有直接使用传统的数据结构,而是针对内存使用效率和操作性能进行了深度优化。
Redis的五种基本数据类型
Redis提供了五种基本数据类型,每种类型都有其对应的底层数据结构实现:
- String(字符串) - SDS(Simple Dynamic String)
- List(列表) - 双向链表 + 压缩列表
- Hash(哈希) - 压缩列表 + 哈希表
- Set(集合) - 整数集合 + 哈希表
- Sorted Set(有序集合) - 压缩列表 + 跳跃表
SDS:Redis的字符串实现
传统C字符串的问题
传统的C字符串存在以下问题:
- 获取长度需要O(n)时间复杂度:需要遍历整个字符串
- 缓冲区溢出:容易造成内存越界
- 二进制不安全:不能存储包含’\0’的数据
- 频繁内存重分配:每次修改都可能需要重新分配内存
SDS的设计优势
Redis使用SDS(Simple Dynamic String)来解决这些问题:
1 2 3 4 5
| struct sdshdr { int len; int free; char buf[]; };
|
SDS的核心特性
- O(1)时间复杂度获取长度:直接读取len字段
- 空间预分配:减少内存重分配次数
- 惰性空间释放:删除字符时不立即释放内存
- 二进制安全:可以存储任意二进制数据
- 兼容C字符串:可以直接使用C字符串函数
SDS的空间优化策略
空间预分配
1 2 3 4 5
| if (newlen < SDS_MAX_PREALLOC) newlen *= 2; else newlen += SDS_MAX_PREALLOC;
|
惰性空间释放
1 2 3 4 5
| void sdstrim(sds s, const char *cset) { }
|
跳跃表:有序集合的核心
跳跃表的设计思想
跳跃表是一种概率性数据结构,通过多层链表实现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;
|
跳跃表的优势
- 查找效率高:平均O(log n)时间复杂度
- 范围查询支持:可以高效进行范围查询
- 实现简单:相比红黑树更容易实现和调试
- 内存局部性好:链表结构具有良好的内存局部性
压缩列表:内存优化的利器
压缩列表的结构
压缩列表是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 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; 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; 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++; } 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
| typedef struct redisObject { unsigned type:4; unsigned encoding:4; unsigned lru:LRU_BITS; int refcount; void *ptr; } robj;
|
性能优化技巧
1. 合理选择数据类型
- String:适合缓存、计数器
- Hash:适合存储对象属性
- List:适合队列、栈
- Set:适合去重、集合运算
- Sorted Set:适合排行榜、范围查询
2. 内存优化策略
1 2 3 4 5
| maxmemory-policy allkeys-lru hash-max-ziplist-entries 512 hash-max-ziplist-value 64 list-max-ziplist-size -2
|
3. 数据结构转换
Redis会根据数据大小自动转换底层数据结构:
1 2
| 小数据 -> 压缩列表 -> 节省内存 大数据 -> 哈希表/跳跃表 -> 提高性能
|
实际应用案例
案例1:用户会话存储
1 2 3 4 5
| 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
| zadd leaderboard 100 "player1" zadd leaderboard 95 "player2" zadd leaderboard 90 "player3"
zrevrange leaderboard 0 9 withscores
|
案例3:消息队列
1 2 3 4 5 6 7
| lpush message_queue "task1" lpush message_queue "task2" lpush message_queue "task3"
task = rpop message_queue
|
总结
Redis的底层数据结构设计体现了以下核心思想:
- 内存效率优先:通过压缩列表等紧凑结构节省内存
- 性能与空间的平衡:根据数据大小动态选择最优结构
- 渐进式优化:避免一次性操作造成的性能抖动
- 类型特化:针对不同使用场景优化数据结构
理解Redis底层数据结构的实现原理,有助于我们:
- 更好地选择合适的数据类型
- 优化内存使用
- 提高应用性能
- 避免常见的使用误区