# 概述《redis 设计与实现》是基于 2.9 版本写作的,现在(2023.06.27)redis 已经更新到 7.0.11 版本了,虽然书中有些介绍已经过时,但是还是可以对 redis 建立起大致的认知的。
# 一、数据结构与对象redis 有五种基本类型:String、List、Hash、Set、ZSet,目前 7 版本还有五种特殊类型:HyperLogLog、Geospatial、Stream、Bitmaps、Bitfields。
# 数据结构五种常用的基本类型,底层使用的数据结构有:SDS(Simple Dynamic String)、list、hashtable、zipList、intset、skiplist。
1 2 3 4 5 6 7 8 9 10 graph TB; String ==> SDS List ==> list List ==> ziplist Hash ==> ziplist Hash ==> hashtable Set ==> hashtable Set ==> intset Zset ==> ziplist Zset ==> skiplist
在 redis3.2 版本之后 List 底层数据采用快表(quicklist)实现。
# SDS简单动态字符串,其(3.2 版本之前)结构是:
1 2 3 4 5 struct sdshdr { unsigned int len; unsigned int free ; char buf[]; };
SDS 与 C 字符串区别:
与 C 字符串不同,使用额外两个变量记录长度,可以使得我们可以 O (1) 时间复杂度级别获取字符串长度。 二进制安全:C 字符串只能使用 ASCII 编码,可能因为二进制文件中可能包含空字符 '\0',导致 C 字符串误以为终止符,而 sds 使用 len 属性判定终止,不会与文件内容冲突。 杜绝缓存区溢出:C 字符串中,如果出现两个字符串在内存中相邻,修改前字符串 1(长度变长)而不分配空间,就会导致字符串 2 内容被修改。SDS 在修改字符串会先判断给定空间是否足够,足够就直接修改,不够就会先分配空间再去修改。 减少字符串修改带来的内存重分配次数:C 字符串并不记录自身长度,每次修改都需要进行内存重分配:增长字符串不进行内存重分配 — 缓冲区溢出,缩小字符串长度不进行内存重分配 — 内存泄露。Redis 作为数据库,如果每次修改都去做内存中分配,就会对性能造成影响,所以 redis 采用空间预分配以及惰性空间释放策略。 # 空间预分配在 SDS 增长字符串长度时,如果发现原本的空间不足以修改,需要内存重分配,除了为 SDS 分配修改所必需的空间,还会分配额外的空间:
如果 SDS 修改之后长度小于 1MB,程序会分配和 len 属性同样大小的未使用空间,此时 SDS 的值等于 free 的值 如果 SDS 修改之后长度大于等于 1MB,会分配 1MB 的未使用空间 执行过 APPEND 命令的字符串会带有额外的预分配空间,这些预分配空间不会被释放,除非该字符串所对应的键被删除,或者等到关闭 Redis 之后,再次启动时重新载入的字符串对象将不会有预分配空间。
# 链表 list作为 List 类型的底层实现,3.2 之后就废用了,使用快表代替。
1 2 3 4 5 typedef struct listNode { struct listNode *prev ; struct listNode *next ; void *value; }
双端链表
1 2 3 4 5 6 typedef struct list { struct listNode *head ; struct listNode *tail ; unsigned long len; }
# 压缩列表 ziplist压缩列表是列表(3.2 版本之前)和 Hash 表底层实现。
大致结构:
zlbytes ztail zllen entry1 ... entryX zlend
zlbytes:压缩列表总字节数 ztail:尾部偏移量 zllen:节点数量 entryX:节点 entry 结构:
previous_entry_length encoding content
previous_entry_length: 前一个节点的长度 encoding:值的编码 content:存储内容(指针指向)
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 #define ZIPLIST_HEADER_SIZE (sizeof(uint32_t)*2+sizeof(uint16_t)) #define ZIPLIST_END_SIZE (sizeof(uint8_t)) unsigned char *ziplistNew (void ) { unsigned int bytes = ZIPLIST_HEADER_SIZE + ZIPLIST_END_SIZE; unsigned char *zl = zmalloc(bytes); ZIPLIST_BYTES(zl) = intrev32ifbe(bytes); ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE); ZIPLIST_LENGTH(zl) = 0 ; zl[bytes-1 ] = ZIP_END; return zl; } 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;
# 快表 quicklist3.2 版本之后作为 List 类型的底层实现,是双端链表和压缩列表的结合,使用压缩列表作为双端链表的节点
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 49 50 51 52 53 54 typedef struct quicklistNode { struct quicklistNode *prev ; struct quicklistNode *next ; unsigned char *entry; size_t sz; unsigned int count : 16 ; unsigned int encoding : 2 ; unsigned int container : 2 ; unsigned int recompress : 1 ; unsigned int attempted_compress : 1 ; unsigned int dont_compress : 1 ; unsigned int extra : 9 ; } quicklistNode; typedef struct quicklistLZF { unsigned int sz; char compressed[]; } quicklistLZF; typedef struct quicklistBookmark { quicklistNode *node; char *name; } quicklistBookmark; typedef struct quicklist { quicklistNode *head; quicklistNode *tail; unsigned long count; unsigned long len; int fill : QL_FILL_BITS; unsigned int compress : QL_COMP_BITS; unsigned int bookmark_count : QL_BM_BITS; quicklistBookmark bookmarks[]; } quicklist; typedef struct quicklistEntry { const quicklist *quicklist; quicklistNode *node; unsigned char *zi; unsigned char *value; long long longval; unsigned int sz; int offset; } quicklistEntry; 快表源码解释原文链接:https:
# 字典 / 哈希表 dict字典是 Set 和 Hash 的底层数据结构。以下是 7 版本的源码。
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 49 struct dict { dictType *type; dictEntry **ht_table[2 ]; unsigned long ht_used[2 ]; long rehashidx; int16_t pauserehash; signed char ht_size_exp[2 ]; void *metadata[]; }; typedef struct dictType { uint64_t (*hashFunction)(const void *key); void *(*keyDup)(dict *d, const void *key); void *(*valDup)(dict *d, const void *obj); int (*keyCompare)(dict *d, const void *key1, const void *key2); void (*keyDestructor)(dict *d, void *key); void (*valDestructor)(dict *d, void *obj); int (*expandAllowed)(size_t moreMem, double usedRatio); size_t (*dictEntryMetadataBytes)(dict *d); } dictType; typedef struct dictEntry { void *key; union { void *val; uint64_t u64; int64_t s64; double d; } v; struct dictEntry *next ; void *metadata[]; } dictEntry;
在书中有提到 dict 是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 typedef struct dict { dictType *type; void *privdata; dictht ht[2 ]; int trehashidx; } dict; typedef struct dictht { dictEntry **table; dictType *type; unsigned long size; unsigned long sizemask; unsigned long used; };
但是在 7 版本并未使用,而是直接使用 dictEntry **ht_table[2];
等属性来构造字典
# rehash当负载因子小于 0.1 时,进行收缩操作。 当未执行 BGSAVE/BGREWRITEAOF 命令时,负载因子大于等于 1 时,进行扩展操作。 当在执行 BGSAVE/BGREWRITEAOF 命令时,负载因子大于等于 5 时,进行扩展操作。 当执行 BGSAVE/BGREWRITEAOF 时提高扩展所需负载因子,主要是因为执行这两个命令时都需要 redis 创建子进程,而此时进行 rehash 操作可能会触发子进程的 "写时复制" 机制。所以此时减少 rehash 操作即可避免不必要的内存写入操作,最大限度的节约内存。
rehash 步骤:
为字典两个哈希表中下标为 1 的未使用的哈希表分配空间:扩展操作:ht [1] 大小 = 大于等于 ht [0].used*2 的 2 的 n 次方。(15 扩展后 32) 收缩操作:ht [1] 大小 = 大于等于 ht [0].used 的 2 的 n 次方。 (12 收缩后 16) 注意是按照已使用也就是元素个数来 rehash,并不是按照 ht [0] 的空间大小来分。 将保存在 ht [0] 的所有键值对 rehash 到 ht [1] 中; 迁移完成后 ht [0] 变为空表,释放 ht [0] 空间,将 ht [1] 设置成 ht [0],并为 ht [1] 创建空白哈希表,为下一次 rehash 做准备。 # 渐进式 rehash为了防止在进行扩展 / 收缩的 rehash 时,由于数据过多造成服务器停止服务,采用了渐进式 rehash 思路。
为 ht [1] 分配空间,此时字典同时持有两个哈希表; 将 rehashidx 设置为 0,表示 rehash 开始; 在 rehash 期间,每次对字典执行增删改查都会额外将 ht [0] 中 rehashidex 索引上的的键值对 rehash 到 ht [1] 中,然后对于 rehashidx++; 所有键值对都被 rehash 到 ht [1] 上之后,rehashidx 置为 - 1,rehash 完成。 # 跳跃表 skiplist跳跃表是 ZSet 的底层数据结构,跳跃表的缺点就是需要的存储空间比较大,属于利用空间来换取时间的数据结构。redis 跳跃表并没有在单独的类(比如 skplist.c) 中定义,而是其定义在 server.h 中:
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 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; typedef struct zset { dict *dict; zskiplist *zsl; } zset;
为什么不使用其他数据结构?
skiplist 和各种平衡树(如 AVL、红黑树等)的元素是有序排列的,而哈希表不是有序的。因此,在哈希表上只能做单个 key 的查找,不适宜做范围查找。所谓范围查找,指的是查找那些大小在指定的两个值之间的所有节点。