Redis设计与实现 -读书笔记

2. 简单动态字符串(SDS)

  • 定义:
struct sdshdr {
   
   
	int len;	// buf中已使用字节数量(不包括末尾'\0')
	int free;	// 未使用字节
	char buf[];	// 字节数组,用于保存字符串
};

被用于保存数据库中的字符串值,或用作缓冲区

  • 与C字符串区别:
  1. 常数复杂度获取字符串长度;
  2. 缓冲区不会溢出
    • 可用空间不够 会扩展空间
  3. 减少修改字符串的内存重分配次数
    • 空间预分配(free,不够就分配min(1MB, len))
    • 惰性空间释放(收回到free,手动api释放)
  4. 二进制安全
    • API都是二进制安全的。SDS使用len判断字符串结束,而不是依靠'\0' ,因此buf保存的是一系列二进制数据
  5. 兼容部分C字符串函数
    • buf也是以'\0' 结尾

3. 链表

typedef struct listNode {
   
   

    // 前置节点
    struct listNode *prev;

    // 后置节点
    struct listNode *next;

    // 节点的值
    void *value;

} listNode;
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;

在这里插入图片描述
多态: 链表节点使用void*指针来保存节点值, 并且可以通过 list 结构的dupfreematch 三个属性为节点值设置类型特定函数, 所以链表可以用于保存各种不同类型的值。

4.字典

底层实现:哈希表
每个哈希表节点保存一个键值对

// dict.h/dictht 
typedef struct dictht {
   
   

    // 哈希表数组
	// 默认实现存放四个指针
    dictEntry **table;

    // 哈希表大小
    unsigned long size;

    // 哈希表大小掩码,用于计算索引值
    // 总是等于 size - 1
    unsigned long sizemask;

    // 该哈希表已有节点的数量
    unsigned long used;

} dictht;

哈希表节点

typedef struct dictEntry {
   
   

    // 键
    void *key;

    // 值
    union {
   
   
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;

    // 指向下个哈希表节点,形成链表
    struct dictEntry *next;

} dictEntry;
  • 字典

字典被广泛用于实现 Redis 的各种功能, 其中包括数据库哈希键
当字典被用作数据库的底层实现, 或者哈希键的底层实现时, Redis 使用MurmurHash2算法来计算键的哈希值。

typedef struct dict {
   
   

    // 类型特定函数
    dictType *type;

    // 私有数据
    void *privdata;

    // 哈希表
    // ht[0] 
    // ht[1] 对ht[0]rehash时使用
    dictht ht[2];

    // rehash 索引   记录rehash进度
    // 当 rehash 不在进行时,值为 -1
    int rehashidx; /* rehashing not in progress if rehashidx == -1 */

} dict;

type属性和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;

在这里插入图片描述

当字典被用作数据库的底层实现, 或者哈希键的底层实现时, Redis 使用MurmurHash2 算法来计算键的哈希值

Redis 计算哈希值和索引值的方法:

// 使用字典设置的哈希函数,计算键 key 的  哈希值
hash = dict->type->hashFunction(key);

// 使用哈希表的 sizemask 属性和哈希值,计算出  索引值
// 根据情况不同, ht[x] 可以是 ht[0] 或者 ht[1]
index = hash & dict->ht[x].sizemask;

链地址法解决冲突,头插法

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

当哈希表的负载因子超过某一个值,哈希表进行rehash扩展收缩
为字典的 ht[1] 哈希表分配空间, 这个哈希表的空间大小取决于要执行的操作, 以及 ht[0] 当前包含的键值对数量 (也即是 ht[0].used属性的值):

  • 如果执行的是扩展操作, 那么 ht[1] 的大小为第一个大于等于 ht[0].used * 2的 2^n (2 的 n 次方幂);
  • 如果执行的是收缩操作, 那么 ht[1] 的大小为第一个大于等于 ht[0].used 的 2^n

将ht[0]的所有键值对rehash到ht[1]中,释放 ht[0] , 将 ht[1] 设置为 ht[0] , 并在 ht[1] 新创建一个空白哈希表。

  • 这个 rehash 动作并不是一次性、集中式地完成的, 而是分多次、渐进式地完成的

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

在进行渐进式 rehash 的过程中, 字典会同时使用 ht[0] 和 ht[1] 两个哈希表, 所以在渐进式 rehash 进行期间, 字典的删除(delete)、查找(find)、更新(update)等操作会在两个哈希表上进行: 比如说, 要在字典里面查找一个键的话, 程序会先在 ht[0] 里面进行查找, 如果没找到的话, 就会继续到 ht[1] 里面进行查找

另外,rehash期间, 新添加到字典的键值对一律会被保存到 ht[1] 里面, 而 ht[0] 则不再进行任何添加操作: 这一措施保证了 ht[0] 包含的键值对数量会只减不增, 并随着 rehash 操作的执行而最终变成空表。

5.跳跃表

跳跃表是可以实现二分查找的有序链表

  • 插入、删除、查找元素的时间复杂度跟红黑树的时间复杂度都是O(logn)

每个元素插入时随机生成它的level;
最底层包含所有的元素;
如果一个元素出现在level(x),那么它肯定出现在x以下的level中;
每个索引节点包含两个指针,一个向下,一个向右;

  • 为什么Redis选择使用跳表而不是红黑树来实现有序集合?

    Redis 中的有序集合(zset) 支持的操作:

    插入一个元素

    删除一个元素

    查找一个元素

    有序输出所有元素

    按照范围区间查找元素(比如查找值在 [100, 356] 之间的数据)

    其中,前四个操作红黑树也可以完成,且时间复杂度跟跳表是一样的。但是,按照区间来查找数据这个操作,红黑树的效率没有跳表高。按照区间查找数据时,跳表可以做到 O(logn) 的时间复杂度定位区间的起点,然后在原始链表中顺序往后遍历就可以了,非常高效。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值