redis(1)——数据结构
redis 是最值得学习的开源项目,
- 它广泛应用
- 它实现了单机缓存数据库,同时支持网络访问、复制、集群、订阅等高级特性
- redis依赖很少,数据结构、日志等基础库也是自行实现,有利于学习
- redis代码精悍、质量高,甚至很难找到优化点。
本文阐释redis数据结构,redis 数据结构设计的一大特点是节省内存。原因是redis作为内存数据库需要节省内存使用(连续内存的数组能减少内存碎片)。这对架构设计同样有借鉴意义,通过节省元数据内存使用来把更多元数据/索引放在内存上,可以大大提高处理的吞吐和延迟。
数据结构
在redis的数据结构中
- sds 最常用做dict的key
- list 作为redis 内部结构使用,例如事件列表,客户端列表
- dict 作为数据库使用
- redisObject 作为数据库的value使用,object可以划分成string, list, hashtable, set, sorted set 五种基本类型- string 通过sds实现
- list 通过quicklist实现;quicklist又是list和ziplist的组合
- hashtable,少量数据通过ziplist实现(减小内存碎片),大量数据通过dict实现
- set,整数且少量数据通过intset实现,大量数据通过dict实现
- sorted set,少量数据通过ziplist实现,大量数据通过dict+skiplist实现(skiplist用来保序)
 
sds, simple dynamic string
sds 就是动态字符串,在C char* 之上的封装,增加了常用的变量和成员函数。sds 在redis使用广泛,最常用的是作为dict的key。
sds有sdshdr8,sdshdr16,sdshdr32等。后面的8, 16, 32 表示sds的最长长度为2^8, 2^16, 2^32(128byte, 64K, 4M)。一般来说,sdshdr8和sdshdr16就够用了
一般来说
| 1 | /* Note: sdshdr5 is never used, we just access the flags byte directly. | 
- buf 实际存放数据,等同于C语言的char *
- 获得长度不需要遍历buf,只需要将(s)-(sizeof(struct sdshdr##T))强制转型为struct sdshdr##T,然后调用sdshdr##T的方法即可
SDS_HDR(T,s) 表示就s 转型为struct sdshdr##T
| 1 | typedef char *sds; | 
创建sds,传入C风格的char *,
- 通过strlen获得C风格字符串长度
- 根据长度确定type
- 构造sdsHeader
- memcpy(s, init, initlen); 将C风格字符串 copy到sds 的buf成员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
 41
 42
 43/* Create a new sds string starting from a null terminated C string. */ 
 sds sdsnew(const char *init) {
 size_t initlen = (init == NULL) ? 0 : strlen(init);
 return sdsnewlen(init, initlen);
 }
 sds sdsnewlen(const void *init, size_t initlen) {
 void *sh;
 sds s;
 char type = sdsReqType(initlen);
 /* Empty strings are usually created in order to append. Use type 8
 * since type 5 is not good at this. */
 if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
 int hdrlen = sdsHdrSize(type);
 unsigned char *fp; /* flags pointer. */
 sh = s_malloc(hdrlen+initlen+1);
 if (!init)
 memset(sh, 0, hdrlen+initlen+1);
 if (sh == NULL) return NULL;
 s = (char*)sh+hdrlen;
 fp = ((unsigned char*)s)-1;
 switch(type) {
 case SDS_TYPE_5: {
 *fp = type | (initlen << SDS_TYPE_BITS);
 break;
 }
 case SDS_TYPE_8: {
 SDS_HDR_VAR(8,s);
 sh->len = initlen;
 sh->alloc = initlen;
 *fp = type;
 break;
 }
 ...
 }
 if (initlen && init)
 memcpy(s, init, initlen);
 s[initlen] = '\0';
 return s;
 }
sds还实现了trim, cmp等常用函数, 是s字符串设计的良好参考
list
双向链表, 增加了len成员变量记录链表的长度
redis 双向链表主要应用在系统结构,例如事件列表fileEvents,定时器事件列表,client列表,slave列表等
| 1 | typedef struct listNode { | 
dict
dict 是kv哈希表,是redis 的核心存储单元。redis 接收到的kv 就是存放在dict结构里。
每个dict对象有两个dictht,每个dictht是一个hash表。使用两个hash表是后续用来rehash。rehash是哈希表扩容,哈希表扩容期间,由于长度变大,会导致key重新排布。rehash的期望是在key重新排布期间,不影响前台IO对哈希表的读写操作。
- redis rehash线程和前台操作使用相同线程,可以避免对dict加锁
- 使用两张表,可以渐进式rehash,每次迁移一个桶。rehash期间,表1的长度是旧长度,表2是新长度。读操作需要先读2,后读1;写操作只需要写表2,删除更新操作也需要同时处理两个表。
- rehash时,只需要复制key和value的指针,不需要拷贝数据,因此对内存的消耗有限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
 41
 42
 43
 44
 45
 46
 47
 48
 49typedef struct dictEntry { 
 void *key;
 union {
 void *val;
 uint64_t u64;
 int64_t s64;
 double d;
 } v;
 struct dictEntry *next;
 } dictEntry;
 typedef struct dictType {
 uint64_t (*hashFunction)(const void *key);
 void *(*keyDup)(void *privdata, const void *key);
 void *(*valDup)(void *privdata, const void *obj);
 int (*keyCompare)(void *privdata, const void *key1, const void *key2);
 void (*keyDestructor)(void *privdata, void *key);
 void (*valDestructor)(void *privdata, void *obj);
 } dictType;
 /* This is our hash table structure. Every dictionary has two of this as we
 * implement incremental rehashing, for the old to the new table. */
 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; /* rehashing not in progress if rehashidx == -1 */
 unsigned long iterators; /* number of iterators currently running */
 } dict;
 /* If safe is set to 1 this is a safe iterator, that means, you can call
 * dictAdd, dictFind, and other functions against the dictionary even while
 * iterating. Otherwise it is a non safe iterator, and only dictNext()
 * should be called while iterating. */
 typedef struct dictIterator {
 dict *d;
 long index;
 int table, safe;
 dictEntry *entry, *nextEntry;
 /* unsafe iterator fingerprint for misuse detection. */
 long long fingerprint;
 } dictIterator;
object
object最常见的使用是作为dict的value存在,
redis key 只有sds一种类型,value有五种类型。
- String字符串,支持SET, GET命令,底层使用sds实现
- List 有序可重复的列表,支持LPUSH, RPOP, LRANGE命令,底层使用quicklist实现
- Hash 键值对集合,支持HSET, HGET, HGETALL,底层使用ziplist, hashtable实现
- Set 无序唯一的集合,支持SADD, SMEMBER,底层使用intset, hashtable实现
- Sorted Set有序唯一的集合(带权重),支持ZADD, ZRANGE, ZRANK命令,底层使用ziplist, skiplist+hashtable组合结构实现
intset 只用来实现set,不用来实现sorted set,原因是sorted set的entry需要有参数作为排序的权重, intset不能支持。
其中redis的hashtable就是dict
| 1 | typedef struct dict { | 
redis 的常用操作命令
| 1 | # 设置key value, value | 
object核心的成员,type,encoding,ptr
| 1 | typedef struct redisObject { | 
encoding有10种, robj可以看成不同实现数据结构的统一接口
| 1 | /* Objects encoding. Some kind of objects like Strings and Hashes can be | 
object 可以序列化到rdb,也可以被load
| 1 | /* Load a Redis object of the specified type from the specified file. | 
quicklist
quicklist 用来实现列表类型value。
quicklist 是一个由多个 ziplist(压缩列表)节点组成的双向链表。quicklist的节点就是 ziplist。
quicklist支持对 ziplist 节点进行 LZF 压缩, 从而节省内存。
(redis是内存数据库,节省内存的使用是它必须考虑的设计)
| 1 | /* quicklistNode is a 32 byte struct describing a ziplist for a quicklist. | 
ziplist
quicklist的元素是ziplist, 除了quicklist, ziplist还作为元素数量少的 hashtable和sorted set的存储结构。
ziplist是一种紧凑的、连续内存存储的线性数据结构,
| 1 | +---------+--------+--------+--------+--------+---------+ | 
每个entry主要由三部分组成, prev_entry_len,encoding,content。content就是一段序列,char *p。
当ziplist存放hashtable时,content是dictEntry。当ziplist存放sorted set是,content就是sds。
| 1 | 
 | 
intset
intset 整数集合用来实现元素数量较小的set,需要满足
- 元素均为整数
- 元素个数小于阈值set-max-intset-entries 512
intset 采用连续内存存储,内部元素有序排布,支持二分查找
intset 支持三种整数类型,按需动态升级:
- INTSET_ENC_INT16:每个元素占 2 字节(范围 -32,768 ~ 32,767)。
- INTSET_ENC_INT32:每个元素占 4 字节(范围 -2^31 ~ 2^31-1)。
- INTSET_ENC_INT64:每个元素占 8 字节(范围 -2^63 ~ 2^63-1)。
| 1 | // intset.h | 
intset add的都是整数
| 1 | /* Insert an integer in the intset */ | 
skiplist和dict结合
跳跃表,和dict 合作实现有序集合
redis 通过哈希表,实现有序集合O(1)的点查询;通过跳跃表,实现集合的有序和范围查询。
哈希表存放zskiplistNode的地址即可,关键成员是sds ele和double score
| 1 | /* ZSETs use a specialized version of Skiplists */ | 
本文标题:redis(1)——数据结构
文章作者:Infinity
发布时间:2025-02-10
最后更新:2025-06-02
原始链接:https://larrystd.github.io/2025/02/10/redis(1)%E2%80%94%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 CN 许可协议。转载请注明出处!
