简单动态字符串
SDS(Simple Dynamic String)是一种由Redis引入的字符串数据结构,旨在提高字符串处理的效率和灵活性。与C语言中的传统字符串(C字符串)相比,SDS提供了一些额外的功能和改进,特别是在内存管理和性能方面。
SDS的结构
struct sdshdr {
int len; // 当前字符串长度
int free; // 剩余可用空间
char buf[]; // 字符数组
};
图示结构
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
SDS字段解析
- len:表示当前SDS字符串的长度,不包括终止
符。 - free:表示在
buf
中未使用的空间量。 - buf:实际存储字符串内容的字符数组,长度为
len
+free
1(加1是为了存储空终止符\0
)。
SDS的特点
- 动态扩展:SDS能够根据需要自动扩展和收缩。当字符串内容增加时,SDS会重新分配足够的空间以容纳新内容,同时保留一定的预留空间,以减少未来的扩展操作。
- 存储长度信息:SDS在结构体中存储了字符串的长度,这使得获取字符串长度的操作时间复杂度为O(1),即常数时间复杂度。
- 二进制安全:SDS能够存储二进制数据,而不仅仅是文本数据。这是因为SDS内部使用的
len
字段来确定字符串的长度,而不是依赖于空终止符\0
。 - 减少缓冲区溢出:由于SDS能够自动管理内存,因此可以有效避免缓冲区溢出的问题。
通过这些特点,SDS在处理字符串操作时比传统的C字符串更为高效和安全。接下来将详细介绍SDS和C字符串的区别。
长度存储:
- C字符串:通过空终止符(
\0
)确定字符串长度,需要遍历整个字符串,时间复杂度为O(n)。 - SDS:通过结构体中的
len
字段存储字符串长度,获取长度的操作时间复杂度为O(1)。
内存****管理:
- C字符串:需要手动管理内存,容易发生缓冲区溢出。
- SDS:自动管理内存,防止缓冲区溢出,提供更安全的字符串操作。
内存****分配:
- C字符串:每次修改字符串内容都可能导致内存重新分配。
- SDS:通过空间预分配和惰性空间释放减少内存重新分配的次数。
二进制安全:
- C字符串:不适合存储二进制数据,因为会误将二进制数据中的
\0
视为字符串终止符。 - SDS:可以存储二进制数据,因为其长度是通过
len
字段确定的,而不是依赖空终止符。
兼容性:
- C字符串:直接使用标准库函数进行字符串操作。
- SDS:兼容部分C字符串函数,同时提供更高效和安全的操作。
常数复杂度获取字符串的长度
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通过以下两种方式减少内存重新分配的次数:
- 空间预分配:
- 当SDS需要扩展时,会多分配一些额外的空间,以便未来的操作可以直接利用这部分空间,而不需要再次进行内存分配。
- 这个操作使得将扩展N次操作的行为,限制到最多执行N次内存分配
s = sdsMakeRoomFor(s, addlen);
- 惰性空间释放:
- 当SDS缩小时,并不会立即释放多余的内存,而是将其标记为未使用的空间,以便将来可以重用。
void sdsIncrLen(sds s, int incr) {
struct sdshdr *sh = (void*)(s - (sizeof(struct sdshdr)));
sh->len += incr;
sh->free -= incr;
}
二进制安全
SDS能够存储和处理二进制数据,而不仅仅是文本数据。这是因为SDS内部使用的len
字段来确定字符串的长度,而不是依赖于空终止符\0
。
兼容部分c字符串函数
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
。
链表节点(listNode)
链表节点包含了节点的数据以及指向前驱和后继节点的指针。其结构体定义如下:
typedef struct listNode {
struct listNode *prev; // 指向前一个节点
struct listNode *next; // 指向后一个节点
void *value; // 节点的值
} listNode;
链表(list)
链表结构体包含了指向头节点和尾节点的指针,以及链表的长度和用于节点值操作的函数指针。其结构体定义如下:
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
)**:链表维护对头节点和尾节点的指针,以及链表的长度。
- 链表节点(
- 操作效率:
- 插入与删除:链表提供了高效的插入和删除操作,尤其是在链表的头部和尾部。插入和删除操作的时间复杂度为O(1)。
- 遍历:通过双向指针,可以高效地从任意节点向前或向后遍历链表。
- 内存****管理:
- 动态扩展:链表可以动态扩展和收缩,适合需要频繁插入和删除操作的场景。
- 内存****利用:每个节点占用额外的指针空间(前后节点指针),相较于单向链表,双向链表在内存使用上有所增加,但操作灵活性更高。
- 应用场景:
- Redis中的链表被广泛应用于实现列表、消息队列、任务队列等数据结构和功能。
- 链表的双向性使得其在某些场景下比单向链表更加高效,例如双向遍历和从任意节点快速访问。
重点回顾
- 链表节点(
listNode
****):包含前驱、后继指针和数据指针。 - 链表**(
list
)**:管理链表的头、尾和长度。 - 效率:双向链表的插入和删除操作时间复杂度为O(1),适合频繁操作的场景。
- 内存****管理:支持动态扩展,但每个节点多占用一些额外内存。
- 应用:适用于实现各种复杂数据结构和功能,如列表和队列。
字典
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
****):- key:存储键的指针。
- v:存储值的联合体,可以存储不同类型的数据。
- next:指向下一个哈希表节点的指针,用于解决哈希冲突。
- 字典(
dict
):- type:指向字典类型的指针,包含操作字典的函数,如比较键、计算哈希值等。
- privdata:用于存储特定于字典的额外数据。
- ht:两个哈希表(
ht[0]
和ht[1]
),用于支持rehash操作。 - size:当前哈希表的大小(桶的数量)。
- sizemask:掩码,用于计算哈希桶的索引。
- used:当前哈希表中键值对的数量。
图示结构
详细解释
- 哈希表:Redis使用两个哈希表来实现字典。每个哈希表包含多个桶,每个桶可以存储一个或多个
dictEntry
节点。哈希表通过哈希函数将键映射到桶中。 - 哈希表****节点:每个
dictEntry
节点包含一个键、一个值和一个指向下一个节点的指针。这个指针用于解决哈希冲突,即多个键被映射到同一个桶时,通过链表结构存储在同一个桶中。 - 字典结构:
dict
结构体包含两个哈希表,用于实现rehash操作,即在哈希表扩展或收缩时,逐步将数据从一个哈希表迁移到另一个哈希表中。type
和privdata
用于定义字典操作和存储额外数据,size
、sizemask
和used
用于管理哈希表的大小和当前数据量。
在Redis中,哈希算法是实现字典(dict)高效查找、插入和删除操作的核心。哈希算法的主要任务是将键映射到哈希表中的桶(bucket)中。下面详细介绍Redis中的哈希算法,包括哈希函数的定义和使用。
哈希函数
哈希函数的作用是将一个键(key)映射到哈希表中的一个位置(桶)。在Redis中,使用了不同的哈希函数来提高哈希表的性能和均匀性。
-
Redis使用的哈希函数:
- Redis使用了
djb2
哈希函数和murmurhash
哈希函数。
- Redis使用了
-
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是一种简单而高效的哈希函数,通过对输入字符进行位移和累加操作来生成哈希值。
- murmurhash:
-
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(用于限制索引在桶数组的范围内)。
图示结构
- Key:输入的键。
- Hash Function:哈希函数将键转换为哈希值。
- Hash Value:哈希函数计算出的哈希值。
- Size Mask:掩码操作将哈希值限制在哈希表的桶范围内。
- Buckets Array:哈希表的桶数组。
- Bucket Index:通过掩码计算得到的桶索引。
- Hash Table Entry:存储在桶中的哈希表条目。
关键点总结
- 哈希函数:Redis使用
djb2
和murmurhash
等哈希函数将键映射到哈希表的桶中,确保高效的存储和查找操作。 - 哈希桶索引计算:通过掩码操作将哈希值限制在哈希表的桶范围内,确保哈希表的均匀性和性能。
- 哈希表:Redis使用两个哈希表(
ht[0]
和ht[1]
)来支持rehash操作,并使用哈希函数和掩码计算桶索引。
哈希算法在Redis中起到了至关重要的作用,确保了字典操作的高效性和哈希表的性能。接下来,将详细介绍如何解决键冲突。
在哈希表中,键冲突(Hash Collision)指的是不同的键通过哈希函数计算得到相同的哈希值,从而映射到哈希表的同一个桶。Redis使用了一些技术来有效地解决键冲突,确保哈希表的性能和正确性。
解决键冲突的策略
Redis采用链式地址法(Chaining)来解决哈希冲突。这种方法在每个桶中维护一个链表,用于存储具有相同哈希值的多个条目。
-
链式地址法(Chaining):
每个哈希表的桶(bucket)是一个链表的头指针。若多个键通过哈希函数计算得到相同的桶索引,则这些键值对会被存储在同一个链表中。
-
链表****节点(
**dictEntry**
):每个节点包含一个键、一个值和一个指向下一个节点的指针。通过这种方式,可以在每个桶中存储多个键值对,从而解决哈希冲突。
哈希冲突的处理流程
- 计算****哈希值:使用哈希函数计算键的哈希值。
- 计算桶索引:通过掩码操作将哈希值映射到哈希表的桶数组中。
- 插入或查找:
- 插入:若桶中已有节点(链表不为空),则将新节点插入到链表的头部或尾部(取决于实现)。如果桶为空,则直接将新节点作为桶的第一个节点。
- 查找:遍历桶中的链表,查找具有相同键的节点。
图示结构
- Hash Function:计算键的哈希值。
- Hash Value:哈希函数生成的哈希值。
- Bucket Index:通过掩码计算得到的桶索引。
- Bucket:哈希表中的桶(链表的头指针)。
- dictEntry:链表中的节点,包含键、值和指向下一个节点的指针。
关键点总结
- 链式地址法:Redis通过在每个桶中维护一个链表来解决哈希冲突,允许多个键值对存储在同一个桶中。
- 插入和查找:在插入新条目时,将其添加到链表中;在查找时,遍历链表以找到具有匹配键的条目。
- 性能:链式地址法在处理键冲突时能够有效地管理哈希表,保证操作的时间复杂度接近O(1),尽管在最坏情况下,所有条目都可能位于同一个链表中,从而使操作变为O(n)。
链式地址法是Redis解决哈希冲突的主要策略,它确保了哈希表在处理冲突时的效率和稳定性。接下来,将详细介绍Redis中的rehash过程。
4. Rehash
在Redis中,rehash
(重新哈希)是动态调整哈希表大小的过程,旨在应对哈希表中键值对数量的变化,保持哈希表的性能和效率。Redis支持哈希表的扩展和收缩,以适应不同的负载需求。