redis源码——数据结构与对象

redis数据库每个键值对都是由对象组成

数据库键总是一个字符串对象;

而数据库键的值则可以是字符串对象、列表对象、哈希对象、集合对象,有序集合对象。

字符串

redis没有直接使用c语言传统字符串,而是自己构建了一种名为简单动态字符串(SDS)的抽象类型。主要是为了解决''的问题。

struct sdshdr {
  int len;         // buf 中已占用空间的长度 0
  int free;       // buf 中剩余可用空间的长度 5
  char buf[];   // 数据空间,最后一个字节保存空字符 'r''e''d''i''s''%0'
};

这样做的好处(长度、结束都由len判断,分配预留free)

  • 可以常数复杂度获取字符串长度。
  • 杜绝缓冲区溢出
  • 减少修改字符串时带来的内存重分配次数
  • 二进制安全
  • 兼容部分c字符串函数

链表

链表提供了高效和节点重排能力,以及顺序性的节点访问方式,并且可以通过增删节点来灵活地调整链表的长度。

链表在redis中的应用非常广泛,比如列表键的底层实现之一就是链表。当一个列表键包含了数量比较多的元素,又或者列表中包含的元素都是比较长的字符串时,redis就会使用链表为列表键的底层实现。

typedef struct listNode {
    struct listNode *prev;     // 前置节点
    struct listNode *next;     // 后置节点
    void *value;                  // 节点的值
} listNode;
typedef struct list {
    void *(*dup)(void *ptr);      // 节点值复制函数
    void (*free)(void *ptr);       // 节点值释放函数
    int (*match)(void *ptr, void *key);      // 节点值对比函数

    listNode *head;        // 表头节点
    listNode *tail;          // 表尾节点
    unsigned long len;         // 链表所包含的节点数量
} list;

redis的链表实现的特性:

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

字典

reds的数据库就是使用字典来作为底层实现的,对数据库的增、删、查、改操作也是构建在对字典的操作之上的。

除了用来表示数据库之外,字典还是哈希键的底层实现之一,当一个哈希键包含的键值对比较多,又或者键值对中的元素都比较长的字符串时,redis就会使用字典作为哈希键的底层实现。

哈希:

typedef struct dictEntry {
    void *key;        //

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

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

字典:

typedef struct dict {
    dictType *type;         // 类型特定函数
    void *privdata;         // 私有数据
    dictht ht[2];             // 哈希表    
    int rehashidx;    // rehash 索引,当 rehash 不在进行时,值为 -1
    int iterators;     // 目前正在运行的安全迭代器的数量
} dict;

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

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

rehashidx它记录了rehash目前的进度,如果目前没有在进行rehash,那么它的值为-1。

跳跃表(skiplist)

https://www.cnblogs.com/losophy/p/10517293.html

整数集合

整数集合是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现。

typedef struct intset {
    uint32_t encoding;    // 编码方式
    uint32_t length;    // 集合包含的元素数量
    int8_t contents[];    // 保存元素的数组
} intset;

压缩列表(为了节约内存)

压缩列表是列表键和哈希键的底层实现之一。

当一个列表键只包含少量列表项,并且每个列表要么就是小整数值,要么就是长度比较短的字符串,那么redis就会使用压缩列表来做列表键的底层实现。 

压缩列表是由一系列特殊编码的连续内存块组成的顺序型数据结构。一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值。

unsigned char *ziplistNew(void) {
    unsigned int bytes = ZIPLIST_HEADER_SIZE+1; // ZIPLIST_HEADER_SIZE 是 ziplist 表头的大小, 1 字节是表末端 ZIP_END 的大小
    unsigned char *zl = zmalloc(bytes);    // 为表头和表末端分配空间

    ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);    // 初始化表属性
    ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);
    ZIPLIST_LENGTH(zl) = 0;

    zl[bytes-1] = ZIP_END;    // 设置表末端

    return zl;
}

redis并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统。

redis还在这对象系统中构建了一个引用计数技术实现的内存回收机制。

typedef struct redisObject {
    unsigned type:4;    // 类型
    unsigned encoding:4;    // 编码
    unsigned lru:REDIS_LRU_BITS;    // 对象最后一次被访问的时间
    int refcount;    // 引用计数
    void *ptr;    // 指向实际值的指针
} robj;

参考:

《redis设计与实现》

原文地址:https://www.cnblogs.com/losophy/p/9539648.html