一、文档介绍

本文仅作为本人读书笔记使用,不对其中内容做解释,记录以本人可以看懂为标准

该书以简明的方式主要介绍了Redis内部的运行机制,从数据结构到服务器构造,值得推荐

                                                                                      第一部分  内部数据结构

Redis内存所使用的数据结构与算法

一、SDS(Simple Dynamic String) 简单动态字符串

1. SDS作用

  • 实现字符串对象。Redis内字符串对象并不代表就是字符串值,字符串对象还可以保存lang类型的值,包含字符串的字符串对象包含的才是SDS值
  • 取代C默认的char*类型。char*类型有很多限制,比如说数据追加与长度计算。在Redis内,客户端传递给服务器的aof缓存、协议内容、回复等都是SDS类型的存储

2. Redis中的字符串

Redis内的字符串不仅包含\0结尾的字符串,还包含简单的字节数组,还包括其他格式的数据等内容

Redi使用SDS类替换C语言默认字符串考虑到两点:高效、二进制安全(程序不对字符串里面保存的数据进行任何假设)

3. SDS实现



typedef char * sds;

struct sdshdr {
    len = 11;
    free = 0;
    buf = "hello world\0";  // buf 的实际长度为 len + 1
};



sds 是 char* 的一个别名

结构体里包含了三个属性:len、free、buffer三个属性

通过len属性可以O(1)的进行长度计算;通过buf分配额外的空间,并使用free记录未使用空间的大小,sds可以让执行追加操作所需的内存重分配次数大大减少;在char *实现中,追加只能通过重分配内存实现

于此同时对SDS的操作必须正确处理len与free属性

4. 如何减少内存重分配次数

创建:当调用set命令创建SDSHDR时,BUF不多申请,刚好给够所需的,free=0

追加:当再次追加的时候,如果free的长度大于所需,就直接append进去,不然的话,会给二倍的内存,但是如果大于SDSHDR允许的MAX_Preallocation,最大预分配,不会翻倍,会再给一个MAX_Preallocation

释放时间:当键值被删除时预分配空间会被删除;当重启Redis时,预分配的空间也会被释放,每个SDS对应的SDSHDR不存在预分配空间,BUFF大小等于所需空间

SDS是Redis对应的字符串表示,SDSHDR是对应的存储类型

二、双端链表

1. 双端链表作用

  • 双端链表是Redis列表(List)结构的底层实现之一,另一个是压缩列表,因为压缩列表占用的额内存更少,在需要的时候才会从压缩列表转换为双端链表
  • 事务模块使用双端链表依序保存输入的命令
  • 服务器模块使用双端链表来保存多个客户端
  • 订阅/发送模块使用双端链表来保存订阅模式的多个客户端
  • 事件模块使用双端链表来保存时间事件(time event)

2. 双端链表的实现

 双端链表是由两部分组成的,list与listNode

redis的设计与实现豆瓣 redis设计与实现电子书_字符串



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;



listNode的value值的类型是void *,方法返回值的类型也是void *,代表对值得类型不做限制

3. 迭代器



typedef struct listIter {

    // 下一节点
    listNode *next;

    // 迭代方向
    int direction;

} listIter;



迭代器内保存一个listNode,并且指明迭代的方向

三、字典

 1. 字典的作用

  • 实现数据库键空间
  • 用作hash类型键的底层实现之一

Redis是一个键值对数据库,数据库中的键值对由字典保存,每个数据库都有一个字典,这个字典称为键空间(Key Space),当用户添加一个键值对到数据库中时,无论键值对是什么类型,程序就会将该键值对添加到键空间

hash类型键的底层实现除了字典外就是压缩列表

2. 字典的实现

字典的实现方式有很多种,例如链表与数组,优点:简单   缺点:只适用于元素不多的情况

                                          哈希表,      优点:高效简单

                                          平衡树,      优点:稳定,排序操作更高效  缺点:实现更复杂

Redis采用哈希表实现字典,哈希表的子结构是dictEntry

dictEntry的实现



/*
 * 哈希表节点
 */
typedef struct dictEntry {

    // 键
    void *key;

    // 值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;          //union关键字 三种中只能包含其中一种

    // 链往后继节点
    struct dictEntry *next;

} dictEntry;



next指针指向另一个dircEntry节点,之所以存在链是因为有的键值可能会哈希成同一值,于是采用链地址法来处理哈希值碰撞问题,当不同的键拥有相同的哈希值时,哈希表就将这些键链接起来

dircht(dirc hash table)的实现



/*
 * 哈希表
 */
typedef struct dictht {

    // 哈希表节点指针数组(俗称桶,bucket)
    dictEntry **table;

    // 指针数组的大小
    unsigned long size;

    // 指针数组的长度掩码,用于计算索引值
    unsigned long sizemask;

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

} dictht;



**table是一个数组,俗称桶(bucket),每一个值对应一个dictEntry结构的指针

redis的设计与实现豆瓣 redis设计与实现电子书_数据库_02

size代表数组大小,sizemask意思不清楚,used就是哈希表现有的键的数量

字典的定义



/*
 * 字典
 *
 * 每个字典使用两个哈希表,用于实现渐进式 rehash
 */
typedef struct dict {

    // 特定于类型的处理函数
    dictType *type;

    // 类型处理函数的私有数据
    void *privdata;

    // 哈希表(2 个)
    dictht ht[2];

    // 记录 rehash 进度的标志,值为 -1 表示 rehash 未进行
    int rehashidx;

    // 当前正在运作的安全迭代器数量
    int iterators;

} dict;



字典的实现使用了两个hash table,0号哈希表是字典主要使用的哈希表,1号哈希表只有当程序对0号哈希表进行rehash的时候才会使用

redis的设计与实现豆瓣 redis设计与实现电子书_字符串_03

3. rehash

rehash:当键非常多,远大于table数组长度时,数组内的每个值将退化成一条链,hash Table的优势将不复存在,于是需要进行rehash操作,对hash table进行扩容,将比率尽量维持在1:1左右

rehash触发的条件有两种:1. 键与数组长度比率ratio>=1 && dict_can_resize为真

                                    2. ratio>=dict_force_resize_ratio dict_force_resize_ratio是强制改变大小的比率

当数据库执行后台持久化任务时,为了最大化利用系统的copy on write机制,程序会暂时将dict_can_resize置为假,避免执行自然resize,总而言之就是为了效率

4. 字典的收缩

与rehash相反

收缩操作是程序手动执行的,扩展操作是自动执行的,收缩程序决定填充率是多少的时候来执行收缩程序

对哈希表的扩展和收缩都是分多次、渐进式的进行的

四、跳跃表

跳跃表是一个有层次的链表,增删改查的时间复杂度都是O(logN)

跳跃表解释:http://blog.jobbole.com/111731/ 

有时间再看吧 得有输出才行啊 干点活吧

跳跃表是为了提高链表的增删改查,对于一个链表来讲,选出一些领导者在上一层,于是在增删改查的时候,首先在上一层进行选择区间之后再下沉到下一层,提高了效率

1. 跳跃表的实现

跳跃表的实现



typedef struct zskiplist {

    // 头节点,尾节点
    struct zskiplistNode *header, *tail;

    // 节点数量
    unsigned long length;

    // 目前表内节点的最大层数
    int level;

} zskiplist;



 跳跃表节点(层)的实现



typedef struct zskiplistNode {

    // member 对象
    robj *obj;

    // 分值
    double score;

    // 后退指针
    struct zskiplistNode *backward;

    // 层
    struct zskiplistLevel {

        // 前进指针
        struct zskiplistNode *forward;

        // 这个层跨越的节点数量
        unsigned int span;

    } level[];

} zskiplistNode;



 

Q&A:

为什么Redis要使用C而不是C++的STL容器?

猜想:可能是基于内存或者效率的考虑吧

SDS如何实现二进制安全的?

跳跃表与平衡树比较?