zoukankan      html  css  js  c++  java
  • 关于redis中的SDS、List、字典

    SDS(simple dynamic string)

    和c语言中的字符串类似,SDS在正常维护字符串之外引入了两个变量free和len,len用于记录当前存储的字符串长度(不包括末尾的‘’),free表示SDS结构中剩余可以分配的内存大小。

    struct sdshdr {
    
        // 记录 buf 数组中已使用字节的数量
        // 等于 SDS 所保存字符串的长度
        int len;
    
        // 记录 buf 数组中未使用字节的数量
        int free;
    
        // 字节数组,用于保存字符串
        char buf[];
    
    };

    关于缓冲区溢出的问题

    与 C 字符串不同, SDS 的空间分配策略完全杜绝了发生缓冲区溢出的可能性: 当 SDS API 需要对 SDS 进行修改时, API 会先检查 SDS 的空间是否满足修改所需的要求, 如果不满足的话, API 会自动将 SDS 的空间扩展至执行修改所需的大小, 然后才执行实际的修改操作, 所以使用 SDS 既不需要手动修改 SDS 的空间大小, 也不会出现前面所说的缓冲区溢出问题。

    内存分配

    空间预分配

    空间预分配用于优化 SDS 的字符串增长操作: 当 SDS 的 API 对一个 SDS 进行修改, 并且需要对 SDS 进行空间扩展的时候, 程序不仅会为 SDS 分配修改所必须要的空间, 还会为 SDS 分配额外的未使用空间。

    其中, 额外分配的未使用空间数量由以下公式决定:

    • 如果对 SDS 进行修改之后, SDS 的长度(也即是 len 属性的值)将小于 MB , 那么程序分配和 len 属性同样大小的未使用空间, 这时 SDS len 属性的值将和 free 属性的值相同。 举个例子, 如果进行修改之后, SDS 的 len 将变成 13 字节, 那么程序也会分配 13 字节的未使用空间, SDS 的 buf 数组的实际长度将变成 13 13 27 字节(额外的一字节用于保存空字符)。
    • 如果对 SDS 进行修改之后, SDS 的长度将大于等于 MB , 那么程序会分配 MB 的未使用空间。 举个例子, 如果进行修改之后, SDS 的 len 将变成 30 MB , 那么程序会分配 MB 的未使用空间, SDS 的 buf 数组的实际长度将为 30 MB MB byte 。

    通过空间预分配策略, Redis 可以减少连续执行字符串增长操作所需的内存重分配次数。

    惰性空间释放

    惰性空间释放用于优化 SDS 的字符串缩短操作: 当 SDS 的 API 需要缩短 SDS 保存的字符串时, 程序并不立即使用内存重分配来回收缩短后多出来的字节, 而是使用 free 属性将这些字节的数量记录起来, 并等待将来使用。

    举个例子, sdstrim 函数接受一个 SDS 和一个 C 字符串作为参数, 从 SDS 左右两端分别移除所有在 C 字符串中出现过的字符。

    比如对于图 2-14 所示的 SDS 值 s 来说, 执行:

     sdstrim(s, "XY"); // 移除 SDS 字符串中的所有 'X' 和 'Y' 

    会将 SDS 修改成图 2-15 所示的样子。

    注意执行 sdstrim 之后的 SDS 并没有释放多出来的 8 字节空间, 而是将这 8 字节空间作为未使用空间保留在了 SDS 里面, 如果将来要对 SDS 进行增长操作的话, 这些未使用空间就可能会派上用场。

    List

    每个链表节点的定义

    typedef struct listNode {
    
        // 前置节点
        struct listNode *prev;
    
        // 后置节点
        struct listNode *next;
    
        // 节点的值
        void *value;
    
    } listNode;

    多个 listNode 可以通过 prev 和 next 指针组成双端链表, 如图 3-1 所示。

    虽然仅仅使用多个 listNode 结构就可以组成链表, 但使用 adlist.h/list 来持有链表的话, 操作起来会更方便:

    typedef struct list {
    
        // 表头节点
        listNode *head;
    
        // 表尾节点
        listNode *tail;
    
        // 链表所包含的节点数量
        unsigned long len;
    
        // 节点值复制函数
        void *(*dup)(void *ptr);
    
        // 节点值释放函数
        void (*free)(void *ptr);
    
        // 节点值对比函数
        int (*match)(void *ptr, void *key);
    
    } list;

    list 结构为链表提供了表头指针 head 、表尾指针 tail , 以及链表长度计数器 len , 而 dup 、 free 和 match 成员则是用于实现多态链表所需的类型特定函数:

    • dup 函数用于复制链表节点所保存的值;
    • free 函数用于释放链表节点所保存的值;
    • match 函数则用于对比链表节点所保存的值和另一个输入值是否相等。

    图 3-2 是由一个 list 结构和三个 listNode 结构组成的链表:

    Redis 的链表实现的特性可以总结如下:

    • 双端: 链表节点带有 prev 和 next 指针, 获取某个节点的前置节点和后置节点的复杂度都是 O(1) 。
    • 无环: 表头节点的 prev 指针和表尾节点的 next 指针都指向 NULL , 对链表的访问以 NULL 为终点。
    • 带表头指针和表尾指针: 通过 list 结构的 head 指针和 tail 指针, 程序获取链表的表头节点和表尾节点的复杂度为 O(1) 。
    • 带链表长度计数器: 程序使用 list 结构的 len 属性来对 list 持有的链表节点进行计数, 程序获取链表中节点数量的复杂度为 O(1) 。
    • 多态: 链表节点使用 void* 指针来保存节点值, 并且可以通过 list 结构的 dup 、 free 、 match 三个属性为节点值设置类型特定函数, 所以链表可以用于保存各种不同类型的值。

     哈希表

    Redis 字典所使用的哈希表由 dict.h/dictht 结构定义:

    typedef struct dictht {
    
        // 哈希表数组
        dictEntry **table;
    
        // 哈希表大小
        unsigned long size;
    
        // 哈希表大小掩码,用于计算索引值
        // 总是等于 size - 1
        unsigned long sizemask;
    
        // 该哈希表已有节点的数量
        unsigned long used;
    
    } dictht;

    哈希表节点使用 dictEntry 结构表示, 每个 dictEntry 结构都保存着一个键值对:

    typedef struct dictEntry {
    
        //
        void *key;
    
        //
        union {
            void *val;
            uint64_t u64;
            int64_t s64;
        } v;
    
        // 指向下个哈希表节点,形成链表
        struct dictEntry *next;
    
    } dictEntry;

    key 属性保存着键值对中的键, 而 v 属性则保存着键值对中的值, 其中键值对的值可以是一个指针, 或者是一个 uint64_t 整数, 又或者是一个 int64_t 整数。

    next 属性是指向另一个哈希表节点的指针, 这个指针可以将多个哈希值相同的键值对连接在一次, 以此来解决键冲突(collision)的问题。

    Redis 中的字典由 dict.h/dict 结构表示:

    typedef struct dict {
    
        // 类型特定函数
        dictType *type;
    
        // 私有数据
        void *privdata;
    
        // 哈希表
        dictht ht[2];
    
        // rehash 索引
        // 当 rehash 不在进行时,值为 -1
        int rehashidx; /* rehashing not in progress if rehashidx == -1 */
    
    } dict;

    type 属性和 privdata 属性是针对不同类型的键值对, 为创建多态字典而设置的:

    • type 属性是一个指向 dictType 结构的指针, 每个 dictType 结构保存了一簇用于操作特定类型键值对的函数, Redis 会为用途不同的字典设置不同的类型特定函数。
    • 而 privdata 属性则保存了需要传给那些类型特定函数的可选参数。
    typedef struct dictType {
    
        // 计算哈希值的函数
        unsigned int (*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;

    ht 属性是一个包含两个项的数组, 数组中的每个项都是一个 dictht 哈希表, 一般情况下, 字典只使用 ht[0] 哈希表, ht[1] 哈希表只会在对 ht[0] 哈希表进行 rehash 时使用。

    除了 ht[1] 之外, 另一个和 rehash 有关的属性就是 rehashidx : 它记录了 rehash 目前的进度, 如果目前没有在进行 rehash , 那么它的值为 -1 。

    Rehash

    扩展和收缩哈希表的工作可以通过执行 rehash (重新散列)操作来完成, Redis 对字典的哈希表执行 rehash 的步骤如下:

    1. 为字典的 ht[1] 哈希表分配空间, 这个哈希表的空间大小取决于要执行的操作, 以及 ht[0] 当前包含的键值对数量 (也即是 ht[0].used 属性的值):
      • 如果执行的是扩展操作, 那么 ht[1] 的大小为第一个大于等于 ht[0].used 2 的 2^n (2 的 n 次方幂);
      • 如果执行的是收缩操作, 那么 ht[1] 的大小为第一个大于等于 ht[0].used 的 2^n 。
    2. 将保存在 ht[0] 中的所有键值对 rehash 到 ht[1] 上面: rehash 指的是重新计算键的哈希值和索引值, 然后将键值对放置到 ht[1] 哈希表的指定位置上。
    3. 当 ht[0] 包含的所有键值对都迁移到了 ht[1] 之后 (ht[0] 变为空表), 释放 ht[0] , 将 ht[1] 设置为 ht[0] , 并在 ht[1] 新创建一个空白哈希表, 为下一次 rehash 做准备。

    哈希表的扩展与收缩

    其中哈希表的负载因子可以通过公式:

    # 负载因子 = 哈希表已保存节点数量 / 哈希表大小
    
    load_factor = ht[0].used / ht[0].size

    计算得出。

    渐进式rehash

    以下是哈希表渐进式 rehash 的详细步骤:

    1. 为 ht[1] 分配空间, 让字典同时持有 ht[0] 和 ht[1] 两个哈希表。
    2. 在字典中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 , 表示 rehash 工作正式开始。
    3. 在 rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] , 当 rehash 工作完成之后, 程序将 rehashidx 属性的值增一。
    4. 随着字典操作的不断执行, 最终在某个时间点上, ht[0] 的所有键值对都会被 rehash 至 ht[1] , 这时程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。

    渐进式 rehash 的好处在于它采取分而治之的方式, 将 rehash 键值对所需的计算工作均滩到对字典的每个添加、删除、查找和更新操作上, 从而避免了集中式 rehash 而带来的庞大计算量。

    参考链接:

    【1】《Redis设计与实现》第二章

  • 相关阅读:
    前台的ID不能与后台的变量重命名,否则发布后会出现 Ambiguous match found.
    Android系统匿名共享内存Ashmem(Anonymous Shared Memory)简要介绍和学习计划
    Scala入门指南
    大数据的关键技术
    大数据技术的发展趋势
    大数据——大价值、大机遇、大变革(全彩)
    大数据与云计算
    Android系统源代码情景分析
    Android硬件抽象层(HAL)概要介绍和学习计划
    Android进程间通信(IPC)机制Binder简要介绍和学习计划
  • 原文地址:https://www.cnblogs.com/lalalatianlalu/p/11843997.html
Copyright © 2011-2022 走看看