redis的数据结构与对象

简单动态字符串

文章目录

  1. SDS的定义

SDS(Simple Dynamic String)是一种由Redis引入的字符串数据结构,旨在提高字符串处理的效率和灵活性。与C语言中的传统字符串(C字符串)相比,SDS提供了一些额外的功能和改进,特别是在内存管理和性能方面。

SDS的结构
struct sdshdr {
    int len;    // 当前字符串长度
    int free;   // 剩余可用空间
    char buf[]; // 字符数组
};
图示结构

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

SDS字段解析
  • len:表示当前SDS字符串的长度,不包括终止在这里插入图片描述
    符。
  • free:表示在buf中未使用的空间量。
  • buf:实际存储字符串内容的字符数组,长度为len + free1(加1是为了存储空终止符\0)。
SDS的特点
  1. 动态扩展:SDS能够根据需要自动扩展和收缩。当字符串内容增加时,SDS会重新分配足够的空间以容纳新内容,同时保留一定的预留空间,以减少未来的扩展操作。
  2. 存储长度信息:SDS在结构体中存储了字符串的长度,这使得获取字符串长度的操作时间复杂度为O(1),即常数时间复杂度。
  3. 二进制安全:SDS能够存储二进制数据,而不仅仅是文本数据。这是因为SDS内部使用的len字段来确定字符串的长度,而不是依赖于空终止符\0
  4. 减少缓冲区溢出:由于SDS能够自动管理内存,因此可以有效避免缓冲区溢出的问题。

通过这些特点,SDS在处理字符串操作时比传统的C字符串更为高效和安全。接下来将详细介绍SDS和C字符串的区别。

  1. SDS和字符串的区别

长度存储

  • 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通过以下两种方式减少内存重新分配的次数:

  1. 空间预分配
    1. 当SDS需要扩展时,会多分配一些额外的空间,以便未来的操作可以直接利用这部分空间,而不需要再次进行内存分配。
    2. 这个操作使得将扩展N次操作的行为,限制到最多执行N次内存分配
s = sdsMakeRoomFor(s, addlen);
  1. 惰性空间释放
    1. 当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的应用极大地提高了系统的性能和可靠性。

链表

  1. 链表和链表节点的实现

Redis 是一个高性能的键值存储系统,使用多种数据结构来实现其功能,其中链表是一种重要的数据结构。链表在 Redis 中用于实现列表类型的数据(List)。下面将详细介绍 Redis 中链表的实现,包括链表和链表节点的实现,并进行总结。

  1. 链表和链表节点的实现

在 Redis 中,链表通过两个主要的结构体来实现:listlistNode

链表节点(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--;
}

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 总结

Redis中的链表实现为双向链表,具备以下特点和优势:

  1. 双向链表结构
    1. 链表节点(listNode****):每个节点包含指向前后节点的指针和一个指向数据的指针。
    2. 链表**(list)**:链表维护对头节点和尾节点的指针,以及链表的长度。
  2. 操作效率
    1. 插入与删除:链表提供了高效的插入和删除操作,尤其是在链表的头部和尾部。插入和删除操作的时间复杂度为O(1)。
    2. 遍历:通过双向指针,可以高效地从任意节点向前或向后遍历链表。
  3. 内存****管理
    1. 动态扩展:链表可以动态扩展和收缩,适合需要频繁插入和删除操作的场景。
    2. 内存****利用:每个节点占用额外的指针空间(前后节点指针),相较于单向链表,双向链表在内存使用上有所增加,但操作灵活性更高。
  4. 应用场景
    1. Redis中的链表被广泛应用于实现列表、消息队列、任务队列等数据结构和功能。
    2. 链表的双向性使得其在某些场景下比单向链表更加高效,例如双向遍历和从任意节点快速访问。

重点回顾

  • 链表节点(listNode****):包含前驱、后继指针和数据指针。
  • 链表**(list)**:管理链表的头、尾和长度。
  • 效率:双向链表的插入和删除操作时间复杂度为O(1),适合频繁操作的场景。
  • 内存****管理:支持动态扩展,但每个节点多占用一些额外内存。
  • 应用:适用于实现各种复杂数据结构和功能,如列表和队列。

字典

  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操作,即在哈希表扩展或收缩时,逐步将数据从一个哈希表迁移到另一个哈希表中。typeprivdata用于定义字典操作和存储额外数据,sizesizemaskused用于管理哈希表的大小和当前数据量。
  1. 哈希算法

在Redis中,哈希算法是实现字典(dict)高效查找、插入和删除操作的核心。哈希算法的主要任务是将键映射到哈希表中的桶(bucket)中。下面详细介绍Redis中的哈希算法,包括哈希函数的定义和使用。

哈希函数

哈希函数的作用是将一个键(key)映射到哈希表中的一个位置(桶)。在Redis中,使用了不同的哈希函数来提高哈希表的性能和均匀性。

  • 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是一种简单而高效的哈希函数,通过对输入字符进行位移和累加操作来生成哈希值。

  • 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使用djb2murmurhash等哈希函数将键映射到哈希表的桶中,确保高效的存储和查找操作。
  • 哈希桶索引计算:通过掩码操作将哈希值限制在哈希表的桶范围内,确保哈希表的均匀性和性能。
  • 哈希表:Redis使用两个哈希表(ht[0]ht[1])来支持rehash操作,并使用哈希函数和掩码计算桶索引。

哈希算法在Redis中起到了至关重要的作用,确保了字典操作的高效性和哈希表的性能。接下来,将详细介绍如何解决键冲突。

  1. 解决键冲突

在哈希表中,键冲突(Hash Collision)指的是不同的键通过哈希函数计算得到相同的哈希值,从而映射到哈希表的同一个桶。Redis使用了一些技术来有效地解决键冲突,确保哈希表的性能和正确性。

解决键冲突的策略

Redis采用链式地址法(Chaining)来解决哈希冲突。这种方法在每个桶中维护一个链表,用于存储具有相同哈希值的多个条目。

  • 链式地址法(Chaining)

    每个哈希表的桶(bucket)是一个链表的头指针。若多个键通过哈希函数计算得到相同的桶索引,则这些键值对会被存储在同一个链表中。

  • 链表****节点(**dictEntry**:每个节点包含一个键、一个值和一个指向下一个节点的指针。通过这种方式,可以在每个桶中存储多个键值对,从而解决哈希冲突。

哈希冲突的处理流程
  1. 计算****哈希值:使用哈希函数计算键的哈希值。
  2. 计算桶索引:通过掩码操作将哈希值映射到哈希表的桶数组中。
  3. 插入或查找
    1. 插入:若桶中已有节点(链表不为空),则将新节点插入到链表的头部或尾部(取决于实现)。如果桶为空,则直接将新节点作为桶的第一个节点。
    2. 查找:遍历桶中的链表,查找具有相同键的节点。
图示结构

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • Hash Function:计算键的哈希值。
  • Hash Value:哈希函数生成的哈希值。
  • Bucket Index:通过掩码计算得到的桶索引。
  • Bucket:哈希表中的桶(链表的头指针)。
  • dictEntry:链表中的节点,包含键、值和指向下一个节点的指针。
关键点总结
  • 链式地址法:Redis通过在每个桶中维护一个链表来解决哈希冲突,允许多个键值对存储在同一个桶中。
  • 插入和查找:在插入新条目时,将其添加到链表中;在查找时,遍历链表以找到具有匹配键的条目。
  • 性能:链式地址法在处理键冲突时能够有效地管理哈希表,保证操作的时间复杂度接近O(1),尽管在最坏情况下,所有条目都可能位于同一个链表中,从而使操作变为O(n)。

链式地址法是Redis解决哈希冲突的主要策略,它确保了哈希表在处理冲突时的效率和稳定性。接下来,将详细介绍Redis中的rehash过程。

4. Rehash

在Redis中,rehash(重新哈希)是动态调整哈希表大小的过程,旨在应对哈希表中键值对数量的变化,保持哈希表的性能和效率。Redis支持哈希表的扩展和收缩,以适应不同的负载需求。

哈希表的扩
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值