SDS(Simple Dynamic String)是一种由Redis引入的字符串数据结构,旨在提高字符串处理的效率和灵活性。与C语言中的传统字符串(C字符串)相比,SDS提供了一些额外的功能和改进,特别是在内存管理和性能方面。
struct sdshdr { int len; // 当前字符串长度 int free; // 剩余可用空间 char buf[]; // 字符数组 };
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
buf
中未使用的空间量。len
+ free
1(加1是为了存储空终止符\0
)。len
字段来确定字符串的长度,而不是依赖于空终止符\0
。通过这些特点,SDS在处理字符串操作时比传统的C字符串更为高效和安全。接下来将详细介绍SDS和C字符串的区别。
长度存储:
\0
)确定字符串长度,需要遍历整个字符串,时间复杂度为O(n)。len
字段存储字符串长度,获取长度的操作时间复杂度为O(1)。内存****管理:
内存****分配:
二进制安全:
\0
视为字符串终止符。len
字段确定的,而不是依赖空终止符。兼容性:
SDS在结构体中存储了字符串的长度,这使得获取字符串长度的操作时间复杂度为O(1),即常数时间复杂度。这比C字符串的O(n)复杂度要高效得多。
int sdslen(const sds s) { struct sdshdr *sh = (void*)(s - (sizeof(struct sdshdr))); return sh->len; }
由于SDS能够自动管理内存,因此可以有效避免缓冲区溢出的问题。每次操作时,SDS都会检查是否有足够的空间,如果没有,就会自动扩展。
SDS通过以下两种方式减少内存重新分配的次数:
s = sdsMakeRoomFor(s, addlen);
void sdsIncrLen(sds s, int incr) { struct sdshdr *sh = (void*)(s - (sizeof(struct sdshdr))); sh->len += incr; sh->free -= incr; }
SDS能够存储和处理二进制数据,而不仅仅是文本数据。这是因为SDS内部使用的len
字段来确定字符串的长度,而不是依赖于空终止符\0
。
SDS兼容部分C字符串函数,使得开发者可以方便地将其与C字符串互换使用。
sds s = sdsnew("hello"); s = sdscat(s, " world"); printf("%s\n", s); // 输出:hello world
SDS通过引入长度存储、动态扩展、空间预分配和惰性空间释放等机制,在处理字符串操作时比传统的C字符串更加高效和安全。同时,SDS的二进制安全特性和兼容部分C字符串函数的设计,使其成为一种灵活且功能强大的字符串数据结构。在Redis中,SDS的应用极大地提高了系统的性能和可靠性。
Redis 是一个高性能的键值存储系统,使用多种数据结构来实现其功能,其中链表是一种重要的数据结构。链表在 Redis 中用于实现列表类型的数据(List)。下面将详细介绍 Redis 中链表的实现,包括链表和链表节点的实现,并进行总结。
在 Redis 中,链表通过两个主要的结构体来实现:list
和 listNode
。
链表节点包含了节点的数据以及指向前驱和后继节点的指针。其结构体定义如下:
typedef struct listNode { struct listNode *prev; // 指向前一个节点 struct listNode *next; // 指向后一个节点 void *value; // 节点的值 } listNode;
链表结构体包含了指向头节点和尾节点的指针,以及链表的长度和用于节点值操作的函数指针。其结构体定义如下:
typedef struct list { listNode *head; // 指向链表的头节点 listNode *tail; // 指向链表的尾节点 void *(*dup)(void *ptr); // 节点值复制函数 void (*free)(void *ptr); // 节点值释放函数 int (*match)(void *ptr, void *key); // 节点值匹配函数 unsigned long len; // 链表的长度(节点数) } list;
链表的基本操作包括创建链表、释放链表、添加节点、删除节点等。
list *listCreate(void) { struct list *list; if ((list = malloc(sizeof(*list))) == NULL) return NULL; list->head = list->tail = NULL; list->len = 0; list->dup = NULL; list->free = NULL; list->match = NULL; return list; }
void listDelNode(list *list, listNode *node) { if (node->prev) node->prev->next = node->next; else list->head = node->next; if (node->next) node->next->prev = node->prev; else list->tail = node->prev; if (list->free) list->free(node->value); free(node); list->len--; }
Redis 提供了在链表头部和尾部添加节点的功能:
list *listAddNodeHead(list *list, void *value) { listNode *node; if ((node = malloc(sizeof(*node))) == NULL) return NULL; node->value = value; if (list->len == 0) { list->head = list->tail = node; node->prev = node->next = NULL; } else { node->prev = NULL; node->next = list->head; list->head->prev = node; list->head = node; } list->len++; return list; } list *listAddNodeTail(list *list, void *value) { listNode *node; if ((node = malloc(sizeof(*node))) == NULL) return NULL; node->value = value; if (list->len == 0) { list->head = list->tail = node; node->prev = node->next = NULL; } else { node->prev = list->tail; node->next = NULL; list->tail->next = node; list->tail = node; } list->len++; return list; }
void listDelNode(list *list, listNode *node) { if (node->prev) node->prev->next = node->next; else list->head = node->next; if (node->next) node->next->prev = node->prev; else list->tail = node->prev; if (list->free) list->free(node->value); free(node); list->len--; }
Redis中的链表实现为双向链表,具备以下特点和优势:
listNode
****):每个节点包含指向前后节点的指针和一个指向数据的指针。list
)**:链表维护对头节点和尾节点的指针,以及链表的长度。listNode
****):包含前驱、后继指针和数据指针。list
)**:管理链表的头、尾和长度。Redis中的字典(dict)是一个重要的数据结构,通常用于存储键值对。Redis的字典实现是基于哈希表的,提供了高效的查找、插入和删除操作。下面详细介绍Redis字典的实现,包括哈希表、哈希表节点和字典本身的结构。
在Redis中,字典实现使用了两个哈希表来存储键值对。这两个哈希表用于支持高效的查找和管理字典中的数据。
typedef struct dictEntry { void *key; // 键 union { void *val; // 值 uint64_t u64; int64_t s64; } v; // 值 struct dictEntry *next; // 指向下一个哈希表节点的指针(用于解决哈希冲突) } dictEntry; typedef struct dict { dictType *type; // 指向字典类型的指针(用于定义操作字典的函数) void *privdata; // 私有数据(可以用来存储特定于字典的额外数据) dictEntry **ht[2]; // 哈希表的数组(支持两个哈希表用于rehash) unsigned long size; // 当前哈希表的大小(桶的数量) unsigned long sizemask; // 哈希表的掩码(用于计算哈希桶的索引) unsigned long used; // 当前哈希表中的键值对数量 } dict;
dictEntry
****): dict
): ht[0]
和 ht[1]
),用于支持rehash操作。dictEntry
节点。哈希表通过哈希函数将键映射到桶中。dictEntry
节点包含一个键、一个值和一个指向下一个节点的指针。这个指针用于解决哈希冲突,即多个键被映射到同一个桶时,通过链表结构存储在同一个桶中。dict
结构体包含两个哈希表,用于实现rehash操作,即在哈希表扩展或收缩时,逐步将数据从一个哈希表迁移到另一个哈希表中。type
和privdata
用于定义字典操作和存储额外数据,size
、sizemask
和used
用于管理哈希表的大小和当前数据量。在Redis中,哈希算法是实现字典(dict)高效查找、插入和删除操作的核心。哈希算法的主要任务是将键映射到哈希表中的桶(bucket)中。下面详细介绍Redis中的哈希算法,包括哈希函数的定义和使用。
哈希函数的作用是将一个键(key)映射到哈希表中的一个位置(桶)。在Redis中,使用了不同的哈希函数来提高哈希表的性能和均匀性。
Redis使用的哈希函数:
djb2
哈希函数和murmurhash
哈希函数。djb2:
unsigned long hashFunction(unsigned char *key) { unsigned long hash = 5381; int c; while ((c = *key++)) hash = ((hash << 5) + hash) + c; /* hash * 33 + c */ return hash; }
- djb2是一种简单而高效的哈希函数,通过对输入字符进行位移和累加操作来生成哈希值。
unsigned int murmurhash2(const void *key, int len, unsigned int seed) { const unsigned int m = 0x5bd1e995; const int r = 24; unsigned int h = seed ^ len; const unsigned char *data = (const unsigned char *)key; while (len >= 4) { unsigned int k = *(unsigned int *)data; k *= m; k ^= k >> r; k *= m; h *= m; h ^= k; data += 4; len -= 4; } switch (len) { case 3: h ^= data[2] << 16; case 2: h ^= data[1] << 8; case 1: h ^= data[0]; h *= m; } h ^= h >> 13; h *= m; h ^= h >> 15; return h; }
- murmurhash是一种非加密哈希函数,具有良好的性能和较低的碰撞率,适合用于哈希表等数据结构。
哈希函数生成的哈希值需要通过掩码操作来确定哈希桶的索引。掩码操作用于将哈希值限制在哈希表的范围内。
unsigned long index = hash & dict->sizemask;
hash
是哈希函数生成的哈希值。
dict->sizemask
是哈希表大小减1(用于限制索引在桶数组的范围内)。
djb2
和murmurhash
等哈希函数将键映射到哈希表的桶中,确保高效的存储和查找操作。ht[0]
和ht[1]
)来支持rehash操作,并使用哈希函数和掩码计算桶索引。哈希算法在Redis中起到了至关重要的作用,确保了字典操作的高效性和哈希表的性能。接下来,将详细介绍如何解决键冲突。
在哈希表中,键冲突(Hash Collision)指的是不同的键通过哈希函数计算得到相同的哈希值,从而映射到哈希表的同一个桶。Redis使用了一些技术来有效地解决键冲突,确保哈希表的性能和正确性。
Redis采用链式地址法(Chaining)来解决哈希冲突。这种方法在每个桶中维护一个链表,用于存储具有相同哈希值的多个条目。
链式地址法(Chaining):
每个哈希表的桶(bucket)是一个链表的头指针。若多个键通过哈希函数计算得到相同的桶索引,则这些键值对会被存储在同一个链表中。
链表****节点(**dictEntry**
):每个节点包含一个键、一个值和一个指向下一个节点的指针。通过这种方式,可以在每个桶中存储多个键值对,从而解决哈希冲突。
链式地址法是Redis解决哈希冲突的主要策略,它确保了哈希表在处理冲突时的效率和稳定性。接下来,将详细介绍Redis中的rehash过程。
在Redis中,rehash
(重新哈希)是动态调整哈希表大小的过程,旨在应对哈希表中键值对数量的变化,保持哈希表的性能和效率。Redis支持哈希表的扩展和收缩,以适应不同的负载需求。
Redis的rehash
过程通过以下步骤实现哈希表的扩展和收缩:
rehash
操作期间交替进行,以避免在迁移过程中影响哈希表的性能。为了避免在rehash
过程中出现长时间的阻塞,Redis采用了渐进式rehash
。渐进式rehash
通过分批迁移条目,降低了对系统性能的影响。
rehash
操作会迁移一定数量的条目,而不是一次性迁移所有条目。rehash
的同时,允许对原哈希表进行读写操作,确保系统的高可用性。rehash
过程,包括迁移数据。rehash
,降低对系统性能的影响,同时允许对哈希表进行操作。rehash
避免了长时间的阻塞,确保系统在rehash
期间的高可用性。Redis中的rehash
机制通过动态调整哈希表大小,确保了哈希表在各种负载下的高效性能和内存利用率。接下来,将详细介绍渐进式rehash
的执行过程及其在操作期间的影响。
在Redis中,rehash
(重新哈希)是哈希表动态调整大小的过程。rehash
的触发条件受当前负载因子和系统状态的影响,特别是在不同情况下的负载因子阈值设定有所不同。以下是重新总结的rehash
条件:
BGSAVE
、BGREWRITEAOF
等)对性能造成严重影响,Redis在后台持久化命令执行期间会提高负载因子的触发阈值,从1提高到5。rehash
,分批迁移数据,减少对系统性能的影响。dictRehash
函数会定期触发迁移过程,确保迁移操作在后台平稳进行。rehash
允许与后台任务(如BGSAVE
、BGREWRITEAOF
等)共存,减少对主线程性能的干扰。rehash
,允许后台任务与rehash
过程并行进行,确保系统的稳定性和高效性。这些条件确保了Redis在不同负载和系统状态下能够动态调整哈希表的大小,优化性能和内存使用。
字典的实现
rehash
在后台分批迁移条目,以减少对系统性能的影响。rehash
允许与后台任务(如BGSAVE
、BGREWRITEAOF
等)并行执行,保持系统的高效性。rehash
)、键值对节点和管理信息。rehash
的实现和条件。这些知识点构成了Redis字典的核心,实现了高效的键值对存储和管理。
跳跃表是一种有效的、随机化的数据结构,支持在对数时间内进行插入、删除和查找操作。以下是跳跃表的详细实现,包括节点结构及其各部分功能的说明,以及如何通过源码理解跳跃表的实现。
跳跃表中的每个节点由以下几个关键部分构成:
跳跃表节点的层数决定了它在跳跃表中的高度。每个节点可以有多个层,最底层(Level 0)包含所有节点。每上一层,节点的数量递减,层数越高,节点能够覆盖的范围越大。
示意图:
每层的前进指针指向当前层中下一个节点。这些指针使得在同一层中可以快速遍历到下一个节点。
节点的跨度指的是在跳跃表中,节点在其层级上的跳跃范围。节点在较高层级上能够跨越更多的节点,这加快了查找的速度。
在跳跃表中,为了支持双向遍历,节点还可以包含后退指针,指向同一层中前一个节点。这使得在查找和删除操作时可以在反方向进行遍历。
每个节点包含一个分值(score)和一个成员(member)。分值用于排序和查找,而成员则是实际存储的数据。
在 Redis 源码中,跳跃表的实现通常包括以下结构体和函数:
zskiplistNode
在 Redis 源码中,跳跃表节点的定义如下:
typedef struct zskiplistNode { sds obj; // 节点的成员对象 double score; // 节点的分值 struct zskiplistNode **forward; // 前进指针数组 struct zskiplistNode *backward; // 后退指针 unsigned int level; // 节点的层数 } zskiplistNode;
obj
:存储成员的数据。score
:存储分值。forward
:指向当前节点在不同层的前进指针数组。backward
:指向当前节点在同一层的后退指针。level
:表示当前节点的层数。zskiplist
跳跃表的定义:
typedef struct zskiplist { struct zskiplistNode *header; // 跳跃表的头节点 struct zskiplistNode *tail; // 跳跃表的尾节点 unsigned long length; // 跳跃表的长度 int level; // 跳跃表的最大层数 } zskiplist;
header
:跳跃表的头节点。tail
:跳跃表的尾节点。length
:跳跃表的节点数量。level
:跳跃表的最大层数。zslInsert
函数实现了将节点插入到跳跃表中。zslFind
函数实现了在跳跃表中查找节点。zslDelete
函数实现了从跳跃表中删除节点。总结
跳跃表通过多层链表结构实现高效的数据存取。每个节点包含多个层,每层有前进和后退指针,以支持快速的查找和遍历操作。节点的层数、前进指针、跨度、后退指针和分值成员是跳跃表的关键组成部分。通过源码可以看到 Redis 中跳跃表的具体实现,包括节点结构和主要操作函数。
跳跃表是一种用于有序集合的高效数据结构,它通过多层链表的设计,使得在对数时间内进行插入、删除和查找操作成为可能。以下是跳跃表的主要实现细节和关键概念:
跳跃表中的每个节点包含以下几个部分:
节点示意图:
跳跃表主要包括以下几个结构部分:
跳跃表示意图:
总结
跳跃表通过多层链表结构实现高效的数据存取。每个节点包含多层链表,每层通过前进和后退指针进行连接,支持快速查找和操作。跳跃表在插入、查找和删除操作上均提供了对数时间复杂度的性能,适合用于实现有序集合和需要高效排序的应用场景。
整数集合(intset)是Redis用来存储整数值的有序集合。它的实现采用了紧凑的内存布局,以便在存储和处理整数集合时节省空间和提高效率。
整数集合的底层数据结构定义如下:
typedef struct intset { uint32_t encoding; // 编码方式 uint32_t length; // 集合中的元素数量 int8_t contents[]; // 保存元素的数组 } intset;
encoding
:表示整数集合中所有整数的编码方式。可能的值包括: INTSET_ENC_INT16
(16位)INTSET_ENC_INT32
(32位)INTSET_ENC_INT64
(64位)length
:整数集合中的元素数量。contents
:实际存储整数值的数组。它是一个灵活数组,长度根据需要动态分配。整数集合的初始化函数如下:
intset *intsetNew(void) { intset *is = zmalloc(sizeof(intset)); is->encoding = INTSET_ENC_INT16; is->length = 0; return is; }
初始化时,整数集合的编码方式为INTSET_ENC_INT16
,元素数量为0。
当向整数集合中添加元素时,可能会触发编码升级。以下是添加元素的代码:
intset *intsetAdd(intset *is, int64_t value, uint8_t *success) { uint8_t valenc = _intsetValueEncoding(value); uint32_t pos; if (success) *success = 1; if (valenc > intrev32ifbe(is->encoding)) { return intsetUpgradeAndAdd(is, value); } else { if (intsetSearch(is, value, &pos)) { if (success) *success = 0; return is; } is = intsetResize(is, intrev32ifbe(is->length)+1); if (pos < intrev32ifbe(is->length)) intsetMoveTail(is, pos, intrev32ifbe(is->length)); } _intsetSet(is, pos, value); is->length = intrev32ifbe(intrev32ifbe(is->length)+1); return is; }
intsetAdd
函数通过_intsetValueEncoding
确定要添加的值的编码方式。intsetUpgradeAndAdd
进行升级并添加元素。整数集合通过二分查找来查找元素:
uint8_t intsetSearch(intset *is, int64_t value, uint32_t *pos) { int min = 0, max = intrev32ifbe(is->length)-1, mid = -1; int64_t cur; if (intrev32ifbe(is->length) == 0) { if (pos) *pos = 0; return 0; } while(max >= min) { mid = (min+max) >> 1; cur = _intsetGet(is, mid); if (value > cur) { min = mid+1; } else if (value < cur) { max = mid-1; } else { break; } } if (value == cur) { if (pos) *pos = mid; return 1; } else { if (pos) *pos = min; return 0; } }
intsetSearch
函数采用二分查找算法在整数集合中查找指定值。pos
。pos
。从整数集合中删除元素的代码如下:
intset *intsetRemove(intset *is, int64_t value, int *success) { uint8_t valenc = _intsetValueEncoding(value); uint32_t pos; if (success) *success = 0; if (valenc <= intrev32ifbe(is->encoding) && intsetSearch(is, value, &pos)) { is = intsetResize(is, intrev32ifbe(is->length)-1); if (pos < intrev32ifbe(is->length)-1) intsetMoveTail(is, pos+1, intrev32ifbe(is->length)-pos-1); is->length = intrev32ifbe(intrev32ifbe(is->length)-1); if (success) *success = 1; } return is; }
intsetRemove
函数首先确定要删除的值的编码方式。intsetResize
函数用于调整整数集合的内存大小:
static intset *intsetResize(intset *is, uint32_t len) { uint32_t size = len*intrev32ifbe(is->encoding); is = zrealloc(is, sizeof(intset)+size); return is; }
该函数根据新的长度重新分配内存,并返回调整后的整数集合。
整数集合在内存中的布局如下:
+----------+----------+----------+ | encoding | length | contents | +----------+----------+----------+ | INT16 | 3 | 2, 5, 9| +----------+----------+----------+
encoding
:16位整数编码length
:集合中有3个元素contents
:实际存储的整数值,按升序排列重点剖析
编码方式:整数集合根据存储的整数值的大小动态选择最小的编码方式,以节省内存空间。支持的编码方式包括16位、32位和64位整数。
有序存储:整数集合中的元素按升序排列,有利于快速查找。
内存****紧凑:采用紧凑的内存布局,减少了内存碎片,提高了存储效率。
灵活升级:支持动态升级编码方式,以适应更大范围的整数值存储需求。
当需要向整数集合中添加一个新元素,并且该元素的编码方式大于当前整数集合的编码方式时,Redis会进行升级操作,以便能够存储新的元素。升级操作可以保证整数集合能够存储更大范围的整数值,同时保持有序性。
升级操作在intsetAdd
函数中实现,当新元素的编码方式大于当前整数集合的编码方式时,会调用intsetUpgradeAndAdd
函数:
static intset *intsetUpgradeAndAdd(intset *is, int64_t value) { uint8_t curenc = intrev32ifbe(is->encoding); uint8_t newenc = _intsetValueEncoding(value); int length = intrev32ifbe(is->length); is = intsetResize(is, length+1); while(length--) _intsetSet64(is, length+1, _intsetGet(is, length, curenc)); is->encoding = intrev32ifbe(newenc); if (value < 0) { _intsetSet(is, 0, value); } else { _intsetSet(is, intrev32ifbe(is->length), value); } is->length = intrev32ifbe(intrev32ifbe(is->length)+1); return is; }
确定当前和新编码方式:
curenc
:当前整数集合的编码方式。newenc
:新元素的编码方式。调整整数集合的大小:
intsetResize
函数重新分配内存,使其能够容纳一个新元素。数据迁移:
更新编码方式:
插入新元素:
更新元素数量:
假设一个整数集合使用16位编码存储了元素2和5,现在需要添加一个值为32768的新元素,这需要升级到32位编码。升级前后的内存布局如下:
升级前:
升级后:
编码升级:升级操作确保整数集合能够存储更大范围的整数值,同时保持集合的有序性。
内存****调整:在升级过程中重新分配内存,保证新的元素可以被存储。
数据迁移:将现有元素从旧编码方式转换为新编码方式,并移动到新的位置。
灵活性:升级过程是灵活的,能够根据需要动态调整集合的编码方式,以适应不同范围的整数值。
Redis整数集合(intset)的升级机制有许多好处,尤其是在处理不同范围的整数时。以下将详细解释升级的灵活性和节约空间的好处。
灵活性是Redis整数集合升级的一个重要特性。这种灵活性体现在以下几个方面:
整数集合通过选择最小的编码方式来存储整数值,从而节约内存空间。这种机制在存储大量小整数时尤其有效。
一个只包含小整数的集合:
上述集合只需要使用16位编码,每个元素占用2字节,总共占用6字节。如果升级到32位编码,内存占用将翻倍,变为12字节。但只在必要时升级,从而避免了这种开销。
灵活性:
节约空间:
编码选择:根据整数值的实际范围动态选择合适的编码方式,以确保高效的存储和处理。
自动升级:当需要存储超出当前编码范围的新值时,整数集合自动升级编码方式,简化了开发者的操作。
内存****紧凑:通过选择最小的编码方式,整数集合可以有效地节约内存空间,尤其是在存储大量小整数时。
Redis整数集合(intset)并不支持自动降级。也就是说,一旦整数集合的编码方式升级到更高的级别,它将保持该级别,即使所有的值都可以用较低的编码方式表示。这是为了简化实现和避免频繁的编码变更带来的开销。
复杂性:实现降级会增加整数集合的复杂性。每次删除元素后,都需要检查是否可以降级,并可能涉及大量的数据迁移。
性能:频繁的编码变更(升级和降级)会影响性能。特别是在频繁插入和删除操作的场景中,降级操作会增加不必要的开销。
稳定性:保持编码方式不变可以提高稳定性,避免频繁的数据迁移带来的潜在问题。
尽管Redis整数集合不支持自动降级,但在某些场景下,开发者可以通过手动重新创建整数集合来实现降级。例如,当集合中的所有值都可以用较低的编码方式表示时,可以手动创建一个新的整数集合,并将所有值插入其中。
如果需要将其降级到32位编码,可以手动重新创建集合:
// 手动降级整数集合的示例代码 intset *intsetDowngrade(intset *is) { intset *new_is = intsetNew(); for (int i = 0; i < intrev32ifbe(is->length); i++) { int64_t value = _intsetGet(is, i); uint8_t success; new_is = intsetAdd(new_is, value, &success); } return new_is; }
尽管Redis整数集合不支持自动降级,但了解如何手动降级可以帮助开发者在需要时优化内存使用。保持整数集合的编码方式稳定可以简化实现,提高性能和稳定性。
压缩列表的构成
Redis中的压缩列表(ziplist)是一种紧凑的内存数据结构,主要用于存储多个简单的值,如字符串和整数。它是一种为了节省内存而设计的高效数据结构。压缩列表通过将多个元素紧凑地存储在一起,从而减少了内存开销。
压缩列表的整体结构包含以下几个部分:
压缩列表的头部包含以下信息:
每个节点的结构由三个主要部分构成:
压缩列表的尾部是一个特殊的节点标记,标志着列表的结束。它的存在使得压缩列表可以高效地在尾部进行操作,如追加新的节点。
以下是压缩列表的简化图示:
mermaid 复制代码 graph TD; A[Header] --> B[Node 1] B --> C[Node 2] C --> D[Node N] D --> E[Tail]
previous_entry_length
、Encoding
和Content
。压缩列表的设计目的是在减少内存开销的同时,保持操作的高效性。它通过紧凑的内存布局和灵活的编码方式,实现了这一目标。
在Redis的压缩列表(ziplist)中,每个节点(entry)由三个主要部分组成:previous_entry_length
、Encoding
和 Content
。这些字段共同决定了节点的存储方式和操作效率。以下是每个部分的详细分析:
previous_entry_length
previous_entry_length
字段表示前一个节点的长度,用于支持快速的节点操作和双向遍历。previous_entry_length
快速找到前一个节点,避免全列表遍历。previous_entry_length
的长度是可变的,根据前一个节点的实际长度决定。它通常使用可变长度的编码方式来存储。假设有一个压缩列表如下:
plaintext 复制代码 Header --> Node 1 --> Node 2 --> Node 3 --> Tail
如果Node 2
的previous_entry_length
是5
字节,表示Node 1
的长度是5字节。
Encoding
Encoding
字段指示节点内容的编码方式。压缩列表支持多种编码方式,以提高存储效率。假设Node 1
包含一个小整数42
,编码方式为1字节编码:
plaintext 复制代码 Encoding: 0x00 (1-byte integer) Content: 42 (binary representation)
Content
Content
字段包含节点的实际数据。它的格式取决于Encoding
字段指定的编码方式。Content
可以存储不同类型的数据,如整数和字符串。Content
字段将包含整数的二进制表示。Content
字段将包含字符串的字节表示。假设Node 2
包含字符串“hello”,Content
字段将存储字符串“hello”的字节表示:
Encoding: 0x01 (string) Content: 68 65 6c 6c 6f (hexadecimal for 'hello')
previous_entry_length
:
Encoding
:
Content
:
Encoding
字段,Content
可以是整数或字符串的二进制表示。在Redis的压缩列表(ziplist)中,“连锁反应”指的是对一个节点的操作如何影响到其他节点。这种现象主要体现在节点的插入、删除和移动操作中。由于压缩列表的结构和编码方式,节点之间的操作可能会引发连锁效应,影响列表的整体结构和性能。
previous_entry_length
,以及后续节点的previous_entry_length
。假设在一个压缩列表中插入新节点:
Header --> Node 1 --> Node 2 --> Node 3 --> Tail
插入新节点Node 1.5
:
Header --> Node 1 --> Node 1.5 --> Node 2 --> Node 3 --> Tail
Node 1.5
的previous_entry_length
需要设置为Node 1
的长度,Node 2
的previous_entry_length
需要更新为Node 1.5
的长度。previous_entry_length
字段,可能导致列表结构的重新计算和调整。previous_entry_length
,以指向删除节点之后的节点。删除Node 2
:
Header --> Node 1 --> Node 3 --> Tail
Node 1
的previous_entry_length
需要更新为指向Node 3
的长度。previous_entry_length
字段,并可能会导致节点的重新编码或重新排列。previous_entry_length
字段。将Node 3
移动到Node 1
和Node 2
之间:
Header --> Node 1 --> Node 3 --> Node 2 --> Tail
Node 1
的previous_entry_length
需要指向Node 3
,Node 3
的previous_entry_length
需要指向Node 2
。连锁反应的复杂性:
previous_entry_length
字段。操作步骤:
previous_entry_length
,并更新列表的头部信息。previous_entry_length
,并更新列表的头部信息。previous_entry_length
,并更新列表的头部信息。性能优化:
Redis对象的主要类型有五种,每种类型都有特定的功能和用例。下面是Redis对象类型的简要介绍:
字符串对象(String)
SET key "value"
列表对象(List)
LPUSH list "value"
哈希对象(Hash)
HSET hash field "value"
集合对象(Set)
SADD set "value"
有序集合对象(Sorted Set)
ZADD zset score "value"
图示:Redis对象类型
编码方式:
redisObject
结构体中。实现:
SDS
(Simple Dynamic String)库来动态管理字符串的内存分配。SDS
提供了动态调整字符串长度的功能,避免频繁的内存分配和释放。// sds结构体示例 typedef struct sdshdr { unsigned int len; // 字符串长度 unsigned int free; // 可用内存 char buf[]; // 存储实际数据 } SDS;
redisObject
结构体中,不需要额外的内存分配。// redisObject结构体示例 typedef struct redisObject { unsigned type:4; // 对象类型 unsigned encoding:4; // 编码方式 unsigned refcount:16; // 引用计数 void *ptr; // 指向实际数据 } RedisObject;
RAW
编码转换为 EMBSTR
编码,以节省内存。EMBSTR
的条件。RAW
编码的字符串直接嵌入 redisObject
结构体中。EMBSTR
的限制时,Redis 会将 EMBSTR
编码转换回 RAW
编码。RAW
编码对象。EMBSTR
中的字符串内容到新的 RAW
编码对象中。RAW
编码的字符串转换为 INT
编码。RAW
编码的字符串解析为整数。INT
编码的对象并存储整数值。RAW
编码时,Redis 将整数值转换为字符串格式。INT
编码的整数转换为字符串。RAW
编码的对象并存储字符串内容。SET
RAW
、EMBSTR
或 INT
)。GET
RAW
、EMBSTR
和 INT
编码)。编码方式:
实现:
// ziplist结构体示例 typedef struct { unsigned char *zl; // 指向压缩列表的指针 unsigned int size; // 列表大小 } Ziplist;
// listNode结构体示例 typedef struct listNode { void *value; // 节点存储的值 struct listNode *prev; // 指向前一个节点的指针 struct listNode *next; // 指向下一个节点的指针 } listNode;
ZIPLIST
编码的列表转换为 LINKEDLIST
编码,以提高操作性能。ZIPLIST
中的所有元素。LINKEDLIST
中。LINKEDLIST
。LINKEDLIST
编码的列表变得较小且操作频率降低时,Redis 会将其转换为 ZIPLIST
编码以节省内存。LINKEDLIST
中的所有元素。ZIPLIST
中。ZIPLIST
。LPUSH / RPUSH
ZIPLIST
或 LINKEDLIST
)。LPOP / RPOP
ZIPLIST
或 LINKEDLIST
)。LRANGE
ZIPLIST
或 LINKEDLIST
)检索指定范围的元素。ZIPLIST
,使用压缩列表的索引进行检索。LINKEDLIST
,遍历链表并收集结果。编码方式:
实现:
// ziplist结构体示例 typedef struct { unsigned char *zl; // 指向压缩列表的指针 unsigned int size; // 哈希大小 } Ziplist;
// dictEntry结构体示例 typedef struct dictEntry { void *key; // 哈希表中的键 void *val; // 键对应的值 } dictEntry; typedef struct dict { dictEntry **table; // 哈希表数组 unsigned int size; // 哈希表大小 } dict;
ZIPLIST
编码的哈希转换为 HT
编码,以提高查找和操作性能。ZIPLIST
中的所有字段和值。HT
哈希表中。HT
。HT
编码的哈希变得较小且字段数量减少时,Redis 会将其转换为 ZIPLIST
编码以节省内存。HT
哈希表中的所有字段和值。ZIPLIST
中。ZIPLIST
。HSET
ZIPLIST
或 HT
)。HGET
ZIPLIST
或 HT
)。HDEL
ZIPLIST
或 HT
)。HGETALL
ZIPLIST
或 HT
)遍历所有字段和值。ZIPLIST
,使用压缩列表的索引进行遍历。HT
,直接遍历哈希表中的所有条目。编码方式:
实现:
INTSET
可以动态调整其存储的整数类型(如从8位整数到16位整数等)。// intset结构体示例 typedef struct intset { uint32_t encoding; // 存储元素的编码方式(如 8位、16位、32位等) uint32_t length; // 集合中的元素数量 int8_t contents[]; // 存储元素的数组 } intset;
// dictEntry结构体示例 typedef struct dictEntry { void *key; // 哈希表中的键(集合元素) void *val; // 对应的值(在集合中通常不使用) } dictEntry; typedef struct dict { dictEntry **table; // 哈希表数组 unsigned int size; // 哈希表大小 } dict;
INTSET
编码的集合转换为 HT
编码,以提高操作性能。INTSET
中的所有整数元素。HT
哈希表中。HT
。HT
编码的集合元素数量减少且所有元素为整数时,Redis 会将其转换为 INTSET
编码以节省内存。HT
哈希表中的所有元素。INTSET
中。INTSET
。SADD
INTSET
或 HT
)。SREM
INTSET
或 HT
)。SMEMBERS
INTSET
或 HT
)遍历所有成员。INTSET
,直接遍历整数数组。HT
,遍历哈希表中的所有条目。SISMEMBER
INTSET
或 HT
)。编码方式:
实现:
// ziplist结构体示例 typedef struct { unsigned char *zl; // 指向压缩列表的指针 unsigned int size; // 有序集合的大小 } Ziplist;
// skiplistNode结构体示例 typedef struct zskiplistNode { void *obj; // 有序集合的元素 double score; // 元素的分数 struct zskiplistNode *forward; // 跳表的下一层节点 } zskiplistNode; typedef struct zskiplist { zskiplistNode *header; // 跳表的头节点 int level; // 跳表的层数 } zskiplist;
ZIPLIST
编码的有序集合转换为 SKIPLIST
编码,以支持高效的范围查询和排序操作。ZIPLIST
中的所有元素及其分数。SKIPLIST
中。SKIPLIST
。SKIPLIST
编码的有序集合变得较小且操作频率降低时,Redis 会将其转换为 ZIPLIST
编码以节省内存。SKIPLIST
中的所有元素及其分数。ZIPLIST
中。ZIPLIST
。ZIPLIST
或 SKIPLIST
)。ZIPLIST
或 SKIPLIST
)进行范围查询。ZIPLIST
,使用压缩列表的索引进行查询。SKIPLIST
,遍历跳表中的节点进行查询。ZIPLIST
或 SKIPLIST
)。ZIPLIST
或 SKIPLIST
)。列表对象(List)
哈希对象(Hash)
集合对象(Set)
有序集合对象(Sorted Set)
Redis 在处理不同类型的对象时,需要确保操作的正确性。为此,Redis 实现了类型检查机制来验证对象的类型。
type
字段,用于表示对象的类型。type
是否为 REDIS_STRING
。// 检查对象类型的宏 #define OBJ_TYPE_CHECK(obj, type) \ if ((obj)->type != (type)) return NULL; // 返回错误或处理逻辑 // 示例:检查一个对象是否是字符串类型 void handleStringCommand(robj *obj) { OBJ_TYPE_CHECK(obj, REDIS_STRING); // 继续处理字符串对象的命令 }
Redis 支持命令的多态性,使得相同的命令可以操作不同类型的对象。这是通过以下机制实现的:
GET
命令对字符串对象和哈希对象的处理逻辑是不同的。// 函数指针定义 typedef void (*commandHandler)(robj *obj); // 字符串类型的命令处理函数 void handleStringCommand(robj *obj) { // 处理字符串类型对象的命令 } // 哈希类型的命令处理函数 void handleHashCommand(robj *obj) { // 处理哈希类型对象的命令 } // 根据对象类型选择处理函数 void processCommand(robj *obj, commandHandler handler) { handler(obj); } // 示例:处理命令 void executeCommand(robj *obj) { if (obj->type == REDIS_STRING) { processCommand(obj, handleStringCommand); } else if (obj->type == REDIS_HASH) { processCommand(obj, handleHashCommand); } // 其他类型处理 }
type
字段来确保操作的正确性。。
在 Redis 中,内存回收是一个重要的机制,用于管理和优化内存的使用,确保系统的稳定性和性能。Redis 的内存回收涉及到对象的生命周期管理和内存的释放。
对象的生命周期管理
// 创建一个新对象并初始化引用计数 robj *createObject(int type, void *ptr) { robj *o = zmalloc(sizeof(robj)); o->type = type; o->ptr = ptr; o->refcount = 1; // 初始引用计数为 1 return o; } // 增加对象的引用计数 void incrRefCount(robj *o) { o->refcount++; } // 减少对象的引用计数并在需要时回收内存 void decrRefCount(robj *o) { if (--o->refcount == 0) { // 释放对象的内存 // 假设释放内存的函数是 freeObject freeObject(o); } }
垃圾回收****策略
// 释放对象的内存 void freeObject(robj *o) { if (o->type == REDIS_STRING) { sdsfree(o->ptr); // 释放字符串对象的内存 } else if (o->type == REDIS_LIST) { // 释放列表对象的内存 } else if (o->type == REDIS_HASH) { // 释放哈希对象的内存 } // 其他对象类型的处理 zfree(o); // 释放对象结构体本身的内存 }
内存****释放
Redis 使用对象共享机制来优化内存使用和提高性能。对象共享的核心思想是避免重复创建相同内容的对象,从而减少内存消耗和提升操作效率。
// 创建并共享对象 robj *createSharedObject(int type, void *ptr) { // 使用全局哈希表查找是否已有相同对象 robj *sharedObj = findSharedObject(ptr); if (sharedObj != NULL) { return sharedObj; // 返回已存在的共享对象 } // 创建新对象并将其添加到共享哈希表 robj *newObj = createObject(type, ptr); addObjectToSharedTable(newObj); return newObj; } // 查找共享对象 robj *findSharedObject(void *ptr) { // 查找共享对象哈希表 // 返回对应的共享对象或 NULL } // 将对象添加到共享哈希表 void addObjectToSharedTable(robj *obj) { // 将对象添加到全局共享哈希表 }
字符串共享:
示例代码:
// 共享常量字符串 robj *sharedOK = createSharedObject(REDIS_STRING, "OK");
列表、哈希等:

对象的空转时长(Idle Time)指的是对象在 Redis 中未被访问或操作的时间长度。Redis 使用这个概念来管理和回收长期不活动的对象,从而优化内存使用和性能。
// 更新对象的空转时间 void updateObjectIdleTime(robj *obj) { obj->last_access_time = getCurrentTime(); // 获取当前时间 } // 获取当前系统时间 time_t getCurrentTime() { return time(NULL); // 返回当前时间戳 }
// 清理超时对象的函数 void periodicCleanup() { time_t now = getCurrentTime(); // 遍历所有对象 for (each object in all_objects) { if (now - object->last_access_time > IDLE_TIMEOUT) { freeObject(object); // 释放超时对象的内存 } } } // 空转超时时长阈值 #define IDLE_TIMEOUT 3600 // 1 小时
# Redis 配置文件示例 # 设置对象的空转时长阈值为 1 小时 idle_timeout 3600