redis之数据结构与对象

112 阅读5分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

数据结构

简单动态字符串

struct sdshdr{
    // 已使用字节数
    int len;
    // 未使用字节数
    int free;
    // char数组,用于保存字符串
    char buff[];
}
  1. O(1)时间复杂度获取字符串长度
  2. 杜绝缓冲区溢出
  3. 通过空间预分配和惰性空间释放减少内存重分配的次数
  4. 二进制安全

链表

typedef struct listNode{
    struct listNode* prev;
    struct listNode* next;
    void* value;
} listNode;

typedef struct list{
    listNode *head;
    listNode *tail;
    unsigned long len;
} list;

字典

typedef struct dict{
    dictType *type;
    void *privdata;
    dictht ht[2];
    int rehashidx; // 没有进行hash时为-1
}

typedef struct dictht{
    // 哈希表数组
    dictEntry **table;
    // 哈希表大小
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;

typedef struct dictEntry{
    void *key;
    union{
        void *val;
        uint64_tu64;
        int64_ts64;
    }
    struct dictEntry *next;
} dictEntry;

采用了MurmurHash2,即使输入是有规律的,算法仍能给出一个很好的随机分布性,计算速度也很快。

采用链表法解决hash冲突

rehash

为了让哈希表的负载因子维持在一个合理的范围,当哈希表保存的键值对太多或太少的时候,程序需要对哈希表进行扩展或者收缩。

步骤:

  1. 为字典的ht[1]哈希表分配空间。扩展时:new_size = 2^n >= ht[0].used * 2; 收缩时:new_size = 2^n >= ht[0].used
  2. 将保存在ht[0]中的所有键值对重新计算哈希值和索引值,并放置在ht[1]的对应位置上
  3. 当ht[0]中的所有键值对都迁移到ht[1]中后,释放ht[0],将ht[1]设置为ht[0],并在ht[1]创建一个空白哈希表,为下一次rehash做准备。

触发机制:

  1. 负载因子>=1且没有执行bgsave或者bgrewriteaof命令,扩展
  2. 负载因子>=5且正在执行bgsave或者bgrewriteaof命令,扩展
  3. 负载因子<0.1,收缩

渐进式rehash

rehash动作并不是一次性、集中式地完成,而是分多次、渐进地完成,将rehash均摊到对字典的每个添加、删除、查找和更新操作上。这样可以避免服务器长时间的停止服务。

使用rehashidx记录rehash的进度,每次增删改查时顺带将rehashidx指向的键值对rehash到ht[1]上。删改查首先在ht[0]上进行查找,没有找到再到ht[1]进行查找;增只在ht[1]上进行,确保ht[0]只减不增,最终变成空表。

跳表

跳表是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
跳表支持平均O(logN),最坏O(N)复杂度的节点查找,还可以通过顺序操作来批量处理节点。
大部分情况下,跳表的效率可以和平衡术相媲美树。因为跳表的实现比平衡术更加简单,所以有不少程序都是用跳表来代替平衡树。

typedef struct zskiplist{
    // header:名义头节点,不存储数据
    struct zskiplistNode *header, *tail;
    unsigned long length;
    int level;
}

typedef struct zskiplistNode{
    // 层
    struct zskiplistLevel{
        struct zskiplistNode *forward;
        // 两个节点之间的距离
        unsigned int span;
    } level[];
    
    struct zskiplistNode *backward;
    double score;
    robj *obj;
} zskiplistNode;

每次创建一个跳表节点时,依据幂次定律随机生成一个介于1和32之间的值作为level数组的大小,这个大小就是层的高度。

整数集合

整数集合是集合键的底层实现之一,底层实现为数组,这个数组以有序、无重复的方式保存集合元素,在需要时,会根据新添加元素的类型改变这个数组的类型,即升级操作,但不支持降级操作。

压缩列表

压缩列表是列表和哈希的底层实现之一,是为了节约内存而开发的,是由一系列的连续内存块组成的顺序型数据结构。 | zlbytes | zltail | zllen | entry1 | entry2 | ... | entryN | zlend |

| previous_entry_length | encoding | content |

借助previous_entry_length可以实现从表尾向表头的遍历。

previous_entry_length可能为1一个字节也可能为5个字节,当有多个连续的、长度介于250-253字节的节点时,进行插入或者删除操作可能会导致连锁更新,但这种操作出现的几率并不高。

对象

字符串

int, row, embstr

embstr:是专门用于保存短字符串的一种优化编码方式,raw编码会调用两次内存分配来分别创建redisObject结构和sdshdr结构,而embstr编码则通过一次内存分配来分配一块连续空间。embstr是只读的

列表

ziplist, linkedlist

编码转换:

  1. 列表保存的所有字符串元素的长度都小于64字节
  2. 元素数量小于512个

满足上述两个元素时使用ziplist,否则使用linkedlsit

哈希

ziplist, hashtable

编码转换:

  1. 保存的所有键值对的长度都小于64字节
  2. 元素数量小于512个

满足上述两个元素时使用ziplist,否则使用hashtable

集合

intset, hashtable

编码转换:

  1. 保存的所有元素都是整数值
  2. 元素数量小于512个

满足上述两个元素时使用intset,否则使用hashtable

有序集合

ziplist, skiplist

性能优化

  1. 内存回收:引用计数实现内存回收机制,在使用的时候释放对象进行内存回收。
  2. 对象共享:将键指向一个现有的值对象,并将值对象的引用数+1。redis只对整数值(0-9999)的字符串对象进行共享。
  3. 对象空转时长:lru,记录对象最后一次被访问的时间。