学Redis的第一天

学Redis的第一天 从数据结构开始

  • 底层数据结构
    • SDS(简单动态字符串)
    • 链表
    • 字典
      • 何时开始rehash
      • 大数据量进行rehash
      • 那么在rehash期间进行CRUD操作是如何进行的呢。
    • 跳跃表
    • 整数集合
    • 如果数组中放置了不同的encoding类型的整数怎么办?
    • 压缩列表
    • 对象(没有对象可以new一个吗?T T)
    • 那么编码的差别是什么?
        • SDS:int(整数值)、raw(长度大于32字节字符串)、embstr(长度小于32字节的字符串)
        • 列表对象:编码可为ziplist(压缩表)或者linkedlist(链表)
        • 哈希对象:ziplist或hashtable(字典)
        • 集合对象:intset(整数集合)或者hashtable(字典)
        • 有序集合对象:ziplist(压缩列表)或者skiplist(跳跃列表与字典组成的zset)
      • Redis操作键命令
      • Redis内存回收
        • 妈也第一部分终于写完了。写了我俩天时间。有空继续写写下面的。如果有错漏啊啥的麻烦评论区见

底层数据结构

SDS(简单动态字符串)

一、结构构成
顾名思义 SDS是为了实现字符串的数据结构,其包含了3个属性 分别是

struct sdshdr {
        // buf 中已占用空间的长度
    int len;
    // buf 中剩余可用空间的长度
    int free;
    // 数据空间
    char buf[];
};
  1. int len 等价于String.length 指代的是SDS中保存的字符串的长度
  2. int free 指代的是SDS未分配的空间大小
  3. char[] buf 其中存储着真正的字符串的内容,以及一个’\0’的空字符。

二、 为什么?
首先使用空字符结尾的原因是可以直接重用一部分c语言字符串库里的一些函数,这样可以不用专门编写函数来实现可重用函数的功能。
有着len属性可将查询SDS长度的时间复杂度,从C语言中的O(n)降低为O(1)。
所以在进行字符串拼接时,可避免如同C语言字符串缓存区溢出的问题,这也是“动态”的实现方法之一,在Redis中进行SDS的拼接时首先检查free的大小与另一字符串的len的大小进行对比。如free>other.len
则可以直接拼接。如果free

  1. 空间预分配
    (1).修改之后的SDS的长度小于1MB那么将会为其分配多一倍的空间。也就是说buf的长度将会是2len加"\0" (buf.length=2*(first.len+other.len)+1byte)。
    (2).修改之后的SDS的长度大于1MB的话,每次增长都只会为其分配多1MB的内存,也就是buf的长度是len加1MB加1byte(buf.length=first.len+other.len+1MB+1byte)。
//SDS的API拼接的是sdscat函数在源码中实际调用的是sdscatlen函数
sds sdscatlen(sds s, const void *t, size_t len) {
        struct sdshdr *sh;
        // 原有字符串长度
    size_t curlen = sdslen(s);
    // 扩展 sds 空间
    // T = O(N)
    s = sdsMakeRoomFor(s,len);
    // 内存不足?直接返回
    if (s == NULL) return NULL;
    // 复制 t 中的内容到字符串后部
    // T = O(N)
    sh = (void*) (s-(sizeof(struct sdshdr)));
    memcpy(s+curlen, t, len);
    // 更新属性
    sh->len = curlen+len;
    sh->free = sh->free-len;
    // 添加新结尾符号
    s[curlen+len] = '\0';
    // 返回新 sds
    return s;
}
//其扩展空间sdsMakeRoomFor函数

sds sdsMakeRoomFor(sds s, size_t addlen) {
    struct sdshdr *sh, *newsh;
     // 获取 s 目前的空余空间长度
    size_t free = sdsavail(s);
    size_t len, newlen;
    // s 目前的空余空间已经足够,无须再进行扩展,直接返回
    if (free >= addlen) return s;
    // 获取 s 目前已占用空间的长度
    len = sdslen(s);
    sh = (void*) (s-(sizeof(struct sdshdr)));
    // s 最少需要的长度
    newlen = (len+addlen);
    // 根据新长度,为 s 分配新空间所需的大小
    if (newlen < SDS_MAX_PREALLOC)
        // 如果新长度小于 SDS_MAX_PREALLOC 
        // 那么为它分配两倍于所需长度的空间
        newlen *= 2;
    else
        // 否则,分配长度为目前长度加上 SDS_MAX_PREALLOC
        newlen += SDS_MAX_PREALLOC;
    // T = O(N)
    newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);
    // 内存不足,分配失败,返回
    if (newsh == NULL) return NULL;
    // 更新 sds 的空余长度
    newsh->free = newlen - len;
    // 返回 sds
    return newsh->buf;
}

  1. 惰性空间释放
    当字符串中的某些字符要被移除时,其占用空间并没有减少。而是将其进行空置处理
    也就是说假如“HelloRedis”,这个字符串的空间占用应该为:
    free=0 len=10 char[] buf= ‘H’ ‘e’ ‘l’ ‘l’ ‘o’ ‘R’ ‘e’ ‘d’ ‘i’ ‘s’ ‘\0’
    那么我们将"Hello"进行移除则之后的空间占用为:
    free=5 len=5 char[] buf= ‘R’ ‘e’ ‘d’ ‘i’ ‘s’ ‘\0’ ‘’ ‘’ ‘’ ‘’ ‘’
    当然也不用担心空间浪费SDS有相对应的API可以将其进行彻底的释放
sds sdstrim(sds s, const char *cset) {
    struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));
    char *start, *end, *sp, *ep;
    size_t len;
    // 设置和记录指针
    sp = start = s;
    ep = end = s+sdslen(s)-1;
    // 修剪, T = O(N^2)
    while(sp <= end && strchr(cset, *sp)) sp++;
    while(ep > start && strchr(cset, *ep)) ep--;
    // 计算 trim 完毕之后剩余的字符串长度
    len = (sp > ep) ? 0 : ((ep-sp)+1);
        // 如果有需要,前移字符串内容
    // T = O(N)
    if (sh->buf != sp) memmove(sh->buf, sp, len);
    // 添加终结符
    sh->buf[len] = '\0';
    // 更新属性
    sh->free = sh->free+(sh->len-len);
    sh->len = len;
    // 返回修剪后的 sds
    return s;
}

SDS其实并不仅仅是保存字符,而是可以拿来保存二进制数据。相比起C语言字符串。SDS可以容纳’/0’字符。如果是C语言的话,当检测到第一个’\0’时将会进行结束处理,从而会丢失后面的数据。
其原因就是采用判断len的数值来判断SDS是否结束。
C字符串与SDS的对比

C字符串 SDS
获取字符串长度的时间复杂度是O(N) 获取字符串长度的时间复杂度是O(1)
API不安全可能会造成缓存区溢出 API安全不会造成缓存区溢出
修改字符串长度必定会执行N次内存重分配 修改字符串长度最多会执行N次内存重分配
只能保存文本数据 文本数据和二进制数据我全都要
可以用库中的全部函数 可以重用库中的部分函数
SDS函数 作用
sdsnew 创建一个给定C字符串的SDS
sdsempty 创建一个空的SDS
sdsfree 释放给定的SDS
sdslen 返回len属性
sdsavail 返回free属性
sdsdup 创建一个SDS副本
sdsclear 清空SDS字符串内容
sdscat 拼接SDS字符串(仅限C字符串)
sdscatsds 拼接字符串到末尾(SDS专属限定)
sdscpy 将C字符串覆盖复制到现有SDS中
sdsgrowzero 用空字符对SDS扩容到指定长度
sdsrange 保留给定区间内的数据,不在区间内的数据清除或覆盖
sdstrim 根据一个C字符串对SDS中进行交集清除
sdscmp 比较俩SDS是否相同

链表

想必大家对链表应该都挺熟悉的了我也不多讲解了直接放底层代码吧

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

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

这一看熟悉的场景就知道底层链表是以双链表形式设计的
其中Redis的链表实现特性如下

  • 双端:获取链表节点的前后节点的时间复杂度O(1)
  • 无环:表头节点的prev与表尾节点的next都指向NULL
  • 表头指针与表位指针:list中存储这head和tail指针,因此获取表头节点与表位节点的时间复杂度为O(1)。
  • 链表长度计数器:list中的len属性记录了list中链表节点的数量,获取链表长度的时间复杂度是O(1)。
  • 多态:节点使用void*来保存节点值,通过dup、free、match、来设置节点类型。链表因此可以保存各种不同类型的值

重点!!

  • 链表常用于实现Redis的各种功能,例如列表键、发布订阅、慢查询、监视器等。

喜闻乐见的API部分

函数 作用
listSetDupMethod 将给定函数设置为链表的节点值复制函数
listGetDupMethod 返回链表当前正在使用的节点值复制函数
listSetFreeMethod 将给定函数设置为链表的节点值释放函数
listGetFreeMethod 返回链表当前正在使用的节点值释放函数
listSetMatchMethod 将给定函数设置为链表的节点值对比函数
listGetMatchMethod 返回链表当前正在使用的节点值对比函数
listLength 返回链表的节点数
listFirst 返回表头节点
listLast 返回表尾节点
listPrevNode 返回给定节点的前置节点
listNextNode 返回给定节点的后置节点
listNodeValue 返回给定节点的值
listCreate 创建空链表
listAddNodeHead 将新节点值添加到给定链表头
listAddNodeTail 将新节点值添加到给定链表尾
listInsertNode 将新节点值添加到给定节点的前或后
listSearchKey 查找并返回包含给定值的节点
listIndex 返回给定索引上的节点
listDelNode 删除指定节点
listRotate 将链表表尾节点弹出设置为表头节点
listDup 复制一个给定链表的副本
listRelease 释放给定链表以及所有节点

字典

字典emmmm是新华字典(雾)
字典实际上是一种有着映射关系的集合类型的数据结构。同比于JAVA中的MAP集合一般是(key,value)的数据结构。C语言中并没有这种类型的数据结构。因此Redis就自己手搓了个字典出来
TIPS:Redis数据库就是以字典作为数据库底层实现,CURD等操作也是构建在字典之上。另外哈希键也是基于字典实现。还有许多应用到字典将后续补充。

  1. 哈希表结构
/*
 * 哈希表
 * * 每个字典都使用两个哈希表,从而实现渐进式 rehash 。
 */
typedef struct dictht {
        // 哈希表数组
    dictEntry **table;
    // 哈希表大小
    unsigned long size;
        // 哈希表大小掩码,用于计算索引值
    // 总是等于 size - 1
    unsigned long sizemask;
    // 该哈希表已有节点的数量
    unsigned long used;
} dictht;

  • table是一个包含distEntry的数组
  • size是table的大小
  • used是当前已有节点的数量
  • sizemask值为size-1 其作用于与哈希值决定键放置于table数组的哪个位置。类似于java中HashMap的对hashcode取模定链表位置。
/*
 * 哈希表节点
 */
typedef struct dictEntry {
        // 键
    void *key;
    // 值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;
    // 指向下个哈希表节点,形成链表
    struct dictEntry *next;
} dictEntry;

哈希节点

  • key中保存键
  • v保存值
  • union中表示着值的类型 可以是指针。或者uint64_t整数又或者int64_t整数
  • next标识着另外一个哈希节点的指针。其目的是为了解决哈希冲突从而以链表的形势存在于数组中

字典!

typedef struct dict {
    // 类型特定函数
    dictType *type;
    // 私有数据
    void *privdata;
    // 哈希表
    dictht ht[2];
    // rehash 索引
    // 当 rehash 不在进行时,值为 -1
    int rehashidx; /* rehashing not in progress if rehashidx == -1 */
    // 目前正在运行的安全迭代器的数量
    int iterators; /* number of iterators currently running */
} dict;
  • type属性是一个指向dictType结构的指针,每个dictType结构保存了用语操作指定类型键值对的函数
    Redis会为用途不同的字典设置不同的函数
  • privdata保存了需要传给dictType的可选参数
  • 以下是dictType的结构
/*
 * 字典类型特定函数
 */
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是一个只包含了两个哈希表的数组。一般情况只用到一个。而另一个有且仅在rehash的时候使用。
  • rehashidx表示了rehash的进度。如未进行rehash操作时其值为-1。
  • rehash(重新散列):步骤如下为字典的空哈希表分配空间。其大小取决于将要进行的操作以及正在使用的哈希表的键值对数量。(未使用的哈希表为ht[1],正在使用的哈希表为[0])
  1. 如果执行扩展操作则大小将为第一个大于等于ht[0].used*2的2n 的值的大小
  2. 如果执行的是收缩操作则大小为第一个大于等于ht[0].used的2n 的值的大小
    将保存在ht[0]的全部键值对rehash到ht[1]上。(rehash重新计算键的哈希值和索引值)然后将计算后的键值对放置于ht[1]对应位置上。紧接着释放ht[0],将ht[1]设置为ht[0]。接着新建一个新的空的ht[1],为下次rehash做准备。

何时开始rehash

  • 服务器没有执行BGSAVE或BGREWRITEAOF命令,且哈希表的负载因子大于等于1。
  • 服务器正在执行BGSAVE命令或BGREWRITEAOF命令,且哈希表的负载因子大于等于5。
  • 负载因子小于0.1时也会对其进行rehash收缩操作
  • 执行BGSAVE或BGREWRITEAOF命令时Redis会创建子进程,并且在子进程存在时大概率会使用(copy-on-write)写时复制技术来优化使用效率。在此同时负载因子限制值会被提高用来避免不必要的内存写入操作。尽量节约内存。
  • 负载因子计算公式:load_factor=ht[0].used/ht[0].size

大数据量进行rehash

  • 当数据量小时进行rehash。直接一步到位当然没问题。那么如果有几百上千万个数据量要进行rehash时一步到位是不可能实现的。因此Redis采用了渐进式rehash设计其步骤如下:
  1. 为备胎分配空间是必须的。让字典同时存在俩个哈希表。
  2. 设置rehashidx为0表示rehash工作开始。
  3. 在rehash期间每次对字典进行CURD操作时,程序会执行对应操作并会顺带的将ht[0]在rehashidx索引上的键值对rehash到ht[1],当本次rehash操作完成时rehashidx值会加一。
  4. 随着字典操作的不断执行。ht[0]全被rehash到ht[1]时。rehashidx的值将会被设置成-1,表示rehash操作完成。备胎成功转正。

那么在rehash期间进行CRUD操作是如何进行的呢。

  • Created:只在ht[1]上执行。
  • Updated:同时执行。
  • Read:同上。优先在ht[0]中查找。
  • Delete:同时执行。
函数 作用
dictCreate 创建字典
dictAdd 添加键值对
dictReplace 添加键值对,如键存在则覆盖值
dictFetchValue 返回给定键的值
dictGetRandomKey 随机获得幸运键值对
dictDelete 删除指点键值对
dictRelease 释放给定字典

跳跃表

跳跃表在Redis中的实现较为少,一个是实现有序集合键,另一个是在集群中作为内部的数据结构。
跳跃表的时间复制度是O(logN)到O(N)。也可以通过顺序性操作来批量处理节点。
当有序集合包的元素数量多,或者有序集合元素中的成员是比较长的字符串时。跳跃表就将会作为有序集合键底层实现。至于有序集合将会在后面再说。

/*
 * 跳跃表
 */
typedef struct zskiplist {
    // 表头节点和表尾节点
    struct zskiplistNode *header, *tail;
    // 表中节点的数量
    unsigned long length;
    // 表中层数最大的节点的层数
    int level;
} zskiplist;
  • header:指向跳跃表的表头节点
  • tail:指向跳跃表的表尾节点
  • level:记录当前表内层数最大的节点的层数
  • length:记录跳跃表的长度。
/*
 * 跳跃表节点
 */
typedef struct zskiplistNode {
    // 成员对象
    robj *obj;
    // 分值
    double score;
    // 后退指针
    struct zskiplistNode *backward;
    // 层
    struct zskiplistLevel {
        // 前进指针
        struct zskiplistNode *forward;
        // 跨度
        unsigned int span;
    } level[];
} zskiplistNode;
  • level[]:表示当前节点有多少层。其数组内部不仅包含元素还包含指向同层次的其他节点的指针
    TIPS:节点创建时level的层数是根据幂次定律随机生成其值介于1-32之间。
  • level[].forward:既是前进的指针指向同层次的下个节点
  • span:记录同层次的下个节点的跨度
  • backward:同理是指向前一个节点的指针
  • score:分值。可理解为优先度(emmm应该吧)。顺序是从小到大排列。也就是说节点排列顺序是按照score数值大小排列。
  • obj:节点保存的成员对象是一个指针
函数 作用
zslCreate 创建一个新的跳跃表
zslFree 释放给定跳跃表
zslInsert 将新节点添加入表中
zslDelete 删除给定成员与分值的节点
zslGetRank 返回指定节点的排位
zslGetElementByRank 返回给定排位的节点
zslIsInRange 给定一个分值范围 检查是否有节点分值在这个范围
zslFirstInRange 给定一个范围返回第一个符合对应分值的节点
zslLastInRange 给定一个范围返回最后一个符合对应分值的节点
zslDeleteRangeByScore 给定一个分值范围删除对应范围内的所有节点
zslDeleteRangeByRank 给定一个排位区间删除对应范围内的所有节点

整数集合

顾名思义,当一个集合只包含整数元素时,并且数量不多时。Redis就会是哟整数集合作为集合键的底层实现。

typedef struct intset {
        // 编码方式
    uint32_t encoding;
    // 集合包含的元素数量
    uint32_t length;
    // 保存元素的数组
    int8_t contents[];
} intset;
  • contents[]:数组是底层实现:整数集合中的每个元素都是其数组的一项元素,各个项有序升序排序。并且不包含重复项。
  • length:表示该集合的元素数量也就是数组的长度
  • encoding:数组的类型由此定义。其取值可能为1、INTSET_ENC_INT16 数组类型即为int16_t类型(-32768——32767)。2、INSET_ENC_INT32 数组类型即为int32_t类型(-2147483648——2147483647)。3、INSET_ENC_INT64数组类型即为int64_t类型(-9223372036854775808——9223372036854775807)(以上数值分别是2的次幂~~)
  • contents[]的大小实际上是length*encoding的位数

如果数组中放置了不同的encoding类型的整数怎么办?

  • 放入一个高位整数时 整个数组集合将会进行升级,将其升级为可容纳其类型的集合:
  1. 根据类型进行数组扩容
  2. 将现有元素进行更新并保持有序性
  3. 放入新元素
    其好处在于可提升整数集合的灵活性并且可以节约内存。
    那么降级是怎么来呢?
    降级是不可能降级的,这辈子都不可能降级的。升级之后的数组空间又大又宽敞。说不定还有好看的小哥哥小姐姐进来。我超喜欢这点的。 ——圣*窃格瓦拉
函数 作用
intsetNew 创建一个新的整数集合
intsetAdd 将给定元素添加进去
insetRemove 删除指定元素
insetFind 检查是否存在指定元素
insetRandom 随机返回一个元素
intsetGet 返回指定索引上的元素
intsetLen 返回元素个数
intsetBlobLen 返回占用内存字节数

压缩列表

压缩列表是列表键和哈希键的底层实现之一。当列表键只包含少量列表项,并且每个项要么小整数要么短字符串的时候压缩列表将会成为底层实现。同理哈希键也是如此。

/* The ziplist is a specially encoded dually linked list that is designed
 * to be very memory efficient. 
  * Ziplist 是为了尽可能地节约内存而设计的特殊编码双端链表。
 ** It stores both strings and integer values,
 * where integers are encoded as actual integers instead of a series of
 * characters. 
 * Ziplist 可以储存字符串值和整数值,
 * 其中,整数值被保存为实际的整数,而不是字符数组。
 * It allows push and pop operations on either side of the list
 * in O(1) time. However, because every operation requires a reallocation of
 * the memory used by the ziplist, the actual complexity is related to the
 * amount of memory used by the ziplist.
 * Ziplist 允许在列表的两端进行 O(1) 复杂度的 push 和 pop 操作。
 * 但是,因为这些操作都需要对整个 ziplist 进行内存重分配,
 * 所以实际的复杂度和 ziplist 占用的内存大小有关。
 * /
 ZIPLIST OVERALL LAYOUT:
 * Ziplist 的整体布局:
 * The general layout of the ziplist is as follows:
 * 以下是 ziplist 的一般布局:
 * =================================
 *
  • < zlbytes > 是一个无符号整数,保存着 ziplist 使用的内存数量。 通过这个值,程序可以直接对 ziplist 的内存大小进行调整,而无须为了计算 ziplist 的内存大小而遍历整个列表
  • < zltail > 保存着到达列表中最后一个节点的偏移量。这个偏移量使得对表尾的 pop 操作可以在无须遍历整个列表的情况下进行。
  • < zllen > 保存着列表中的节点数量。 当 zllen 保存的值大于 2**16-2 时,程序需要遍历整个列表才能知道列表实际包含了多少个节点。
  • < zlend >用于标记压缩列表的尾端
/*
  保存 ziplist 节点信息的结构
 */
typedef struct zlentry {
    // prevrawlen :前置节点的长度
    // prevrawlensize :编码 prevrawlen 所需的字节大小
    unsigned int prevrawlensize, prevrawlen;
    // len :当前节点值的长度
    // lensize :编码 len 所需的字节大小
    unsigned int lensize, len;
    // 当前节点 header 的大小
    // 等于 prevrawlensize + lensize
    unsigned int headersize;
    // 当前节点值所使用的编码类型
    unsigned char encoding;
    // 指向当前节点的指针
    unsigned char *p;
} zlentry;
  • prevrawlen:记录了前个节点的长度
  • prevrawlensize:记录了prevrawlen的大小,其值为1字节或者5字节,取决于prevrawlen的长度。
    如果prevrawlen小于254字节即为1字节。大于254字节即为5字节,与此同时最高字节位将会被标记为
    0xFE表示这是个5字节长的属性,低位才是真正的长度。
  • 也因此拥有了方便的从表尾向表头遍历的操作。
  1. 首先获得表尾节点指针,也就是起始地址的指针+< zltail >的值得出
  2. 其次分别减去prevrawlensize与prevrawlen值获得前一个节点的指针位置
  3. 循环往复就遍历了整个列表
  • content:保存节点的值可为字节数组或者整数。类型由encoding决定
  • 注意:对压缩节点的插入和删除操作有可能会触发连锁更新的操作
  • 连锁更新:是有着连续的250-253字节大小的节点。突然插入一个大节点使得prevrawlensize不断连续更新为5字节大小的连锁反应。
函数 作用
ziplistNew 创建新的压缩列表
ziplistPush 创建新节点并将其添加入列表头尾
ziplistInsert 将给定值的新节点插入到指定节点之后
ziplistIndex 返回给定索引的节点
ziplistFind 在压缩列表查找并返回包含给定值的节点
ziplistNext 返回下个节点
ziplistPrev 返回前个节点
ziplistGet 获取节点值
ziplistDelete 删除指定节点
ziplistDeleteRange 删除连续节点
ziplistBlobLen 返回占用内存字节数
ziplistLen 返回节点数量

对象(没有对象可以new一个吗?T T)

Redis使用对象来表示数据库中的键和值,当我们在创建键值对时,我们会创建两个对象,一个是键对象一个是值对象。其结构如代码所示:

typedef struct redisObject {
    // 类型
    unsigned type:4;
    // 编码
    unsigned encoding:4;
    // 对象最后一次被访问的时间
    unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
    // 引用计数
    int refcount;
    // 指向实际值的指针
    void *ptr;
} robj;
  • type:type记录了对象的类型。{REDIS_STRING,REDIS_LIST,REDIS_HASH_REDIS_SET,REDIS_ZSET}
    分别代表字符串对象、列表对象、哈希对象、集合对象、有序集合对象。
    然而KEY总会是字符串对象。而VALUE各类对象都有可能。
    TIPS: 可以用TYPE指令来查看值的类型 type KEY
  • ptr:ptr指针指向底层的实现结构~~也就是上面那些
  • encoding:REDIS_ENCODING_(INT,EMBSTR,RAW,HT,LINKEDLIST,ZIPLIST,INTSET,SKIPLIST)
    等编码格式。TIPS:可以用OBJECT ENCODING来查询编码格式。
    使用encoding来设定编码而不是固定编码可以提高Redis的灵活性和效率。

那么编码的差别是什么?

SDS:int(整数值)、raw(长度大于32字节字符串)、embstr(长度小于32字节的字符串)
命令 int embstr raw
SET 使用对应编码保存 使用对应编码保存 使用对应编码保存
GET 拷贝值,将拷贝转化为字符串并返回 直接返回字符串 直接返回字符串
APPEND 转为raw编码使用raw模式进行操作 转为raw编码使用raw模式进行操作 调用sdscatlen函数进行追加
INCRBYFLOAT 取值并转化为浮点数进行加法计算与保存 取出值并尝试转化,成功如int类型,失败则返回错误 取出值并尝试转化,成功如int类型,失败则返回错误
INCRBY 整数值进行加法计算并保存 返回错误 返回错误
DECRBY 整数值进行减法计算并保存 返回错误 返回错误
STRLEN 拷贝值,将拷贝转化为字符串并返回其长度 调用sdslen函数返回长度 调用sdslen函数返回长度
SETRANGE 转为raw编码使用raw模式进行操作 转为raw编码使用raw模式进行操作 将字符串特定索引上的值设置为给定字符
GETRANGE 拷贝值,将拷贝转化为字符串并返回其索引字符 返回其索引字符 返回其索引字符
列表对象:编码可为ziplist(压缩表)或者linkedlist(链表)
  • ziplist:使用条件为元素的长度都小于64字节和列表对象保存的数量小于512个。
  • linkedlist:其他情况都为linkedilist。
    TIPS:ziplist的条件可以修改!打开redis.conf 设置以下的值即可!
#Similarly to hashes, small lists are also encoded in a special way in order
#to save a lot of space. The special representation is only used when
#you are under the following limits:
list-max-ziplist-entries 512
list-max-ziplist-value 64
命令 ziplist linkedlist
LPUSH 调用ziplistpush函数,将新元素添加入表头 调用listAddNodeHead函数,将新元素添加入表头
RPUSH 调用ziplistpush函数,将新元素添加入表尾 调用listAddNodeTail函数,将新元素添加入表尾
LPOP 调用ziplistIndex函数定位表头节点返回并调用ziplistDelete函数,将表头删除 调用listFirst函数定位表头节点返回并调用listDelNode函数,将表头删除
RPOP 调用ziplistIndex函数定位表尾节点返回并调用ziplistDelete函数,将表尾删除 调用listLast函数定位表头节点返回并调用listDelNode函数,将表头删除
LINDEX 调用ziplistIndex定位指定节点并返回 调用listIndex定位指定节点并返回
LLEN 调用ziplistLen函数返回长度 调用listLength函数返回长度
LINSERT 插入于表头表尾时调用ziplistpush函数,其余情况调用 ziplistInsert 调用listInsertNode函数插入指定位置
LREM 遍历节点并调用ziplistDelete函数删除指定节点 遍历节点并调用listDelNode函数删除指定节点
LTRIM 调用ziplistDeleteRange函数删除不在范围内节点 调用listDelNode函数删除不在范围内节点
LSET 调用ziplistDelete函数先删除指定索引节点,并 调用 ziplistInsert添加新节点于此索引 调用listIndex定位指定节点并赋值更新节点值
哈希对象:ziplist或hashtable(字典)
  • ziplist:使用条件为元素的长度都小于64字节和列表对象保存的数量小于512个。
  • hashtable:其他情况都为hashtable。
    TIPS:ziplist的条件可以修改!打开redis.conf 设置以下的值即可!
# Hashes are encoded using a memory efficient data structure when they have a
# small number of entries, and the biggest entry does not exceed a given
# threshold. These thresholds can be configured using the following directives.
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
命令 ziplist hashtable
HSET 连续调用两次 ziplistpush函数分别将键与值压入表尾 调用dictAdd函数添加新节点进入字典
HGET 先调用ziplistFind函数查找键并ziplistNext函数获得值返回 调用dictFind函数查找指定键调用dictGetVal函数返回值
HEXISTS 调用ziplistFind函数查找键并返回是否存在 调用dictFind函数查找指定键并返回是否存在
HDEL 调用ziplistFind函数查找键删除键节点以及值节点 调用dictDelete函数删除指定键值对
HLEN 调用ziplistLen函数获得长度 并除于2 返回值 调用dictSize函数获得键值对数量
HGETALL 遍历并使用ziplistGet函数返回所有节点 遍历字典用dictGetKey返回键并调用dictGetVal函数返回值
集合对象:intset(整数集合)或者hashtable(字典)
  • intset:使用条件全为整数和对象保存的数量小于512个。
  • hashtable:其他情况都为hashtable。
    TIPS:intset的条件可以修改!打开redis.conf 设置以下的值即可!
# Sets have a special encoding in just one case: when a set is composed
# of just strings that happens to be integers in radix 10 in the range
# of 64 bit signed integers.
# The following configuration setting sets the limit in the size of the
# set in order to use this special memory saving encoding.
set-max-intset-entries 512
命令 intset hashtable
SADD 调用intsetAdd函数,添加元素 调用dictAdd函数添加新键进入字典值为NULL
SCARD 调用intsetLen函数,返回数量 调用dictSize函数获得键值对数量
SISMEMBER 调用intsetFind查找指定元素返回是否存在 调用dictFind函数查找指定键并返回是否存在
SMEMBERS 遍历整个集合调用intsetGet返回元素 遍历字典用dictGetKey返回键作为集合元素
SRANDMEMBER 调用intsetRandom随机返回一个元素 调用dictGetRandomKey随机返回一个键
SPOP 调用intsetRandom随机返回一个元素并intsetRomove函数删除 调用dictGetRandomKey随机返回一个键并调用dictDelete删除指定键值对
SREM intsetRomove函数删除 调用dictDelete删除指定键值对
有序集合对象:ziplist(压缩列表)或者skiplist(跳跃列表与字典组成的zset)
  • ziplist:结构为连续两个节点第一个节点保存其成员第二个节点保存元素的分值。使用条件为元素的长度都小于64字节和列表对象保存的数量小于128个。
  • skiplist:其他情况都为skiplist,skiplist中的跳跃表按分值保存了所有集合元素、字典创建了成员与分值的映射,其键为元素值为分值其好处在于可以结合两者优点查找快并有序。另外采用共享指针的设计使得内存不会浪费。
  • TIPS:ziplist的条件可以修改!打开redis.conf 设置以下的值即可!
 # Similarly to hashes and lists, sorted sets are also specially encoded in
#order to save a lot of space. This encoding is only used when the length and
#elements of a sorted set are below the following limits:
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
命令 ziplist skiplist
ZADD 调用ziplistInsert两次分别压入成员与分值 调用zslInsert添加到跳跃表并调用dictAdd关联进字典
ZCARD 调用ziplistLen函数获得长度 并除于2 返回值 返回跳跃表的length属性
ZCOUNT 遍历统计分值在给定范围内的节点数量 遍历统计分值在给定范围内的节点数量
ZRANGE 遍历并返回给定索引内的元素 遍历并返回给定索引内的元素
ZREVRANGE 反向遍历并返回给定索引内的元素 反向遍历并返回给定索引内的元素
ZRANK 遍历并记录节点数量返回数量为其排名 遍历并记录节点数量返回数量为其排名
ZREVRANGE 反向遍历并记录节点数量返回数量为其排名 反向遍历并记录节点数量返回数量为其排名
ZREM 遍历并删除成员与分值 遍历跳跃表并删除节点并取消字典中的键值关联
ZSCORE 遍历查找对应元素并取next的分值 字典中取出其分值

Redis操作键命令

  • 普适性命令:DEL、SET等基于类型的多态命令。可处理不同类型的键。
  • 特殊性命令:LLEN等基于编码的多态命令。使用时会先检查键的类型是否匹配。再进行处理不同编码的键。

Redis内存回收

  • C语言木的自动回收功能。所以Redis自己手搓的了个引用计数器 (refcount)。结构在上面自己找
  • 创建新对象时refount将会被初始化为1,被一个新程序使用时将会增一,不被使用时将会减一。当它为0时将会释放其对象内存
函数 作用
incrRefcount 加一
decrRefcount 减一,为0释放
resetRefcount 重置对象引用

因此为了节约内存用以引用共享对象来处理。
Redis初始化服务器时会简历1W个字符串对象,其包含了0-9999的整数值。供以应用
TIPS:可以修改Redis.h文件中的数值来修改数量。

#define REDIS_SHARED_INTEGERS 10000
  • 空转时长,结构在上面自己找。
  • lru:记录了最后一次被访问的时间。可以使用OBJECT IDLETIME命令查看,本命令是特殊命令不会修改其值。目的是为了让服务器回收其空间。其基本要求是服务器打开MAXMEMORY选项并且回收内存算法为volatile-lru或者allkeys-lru。当内存超过上限时将会优先释放空转时长高的对象回收内存。
  • 有想法可以看看redis.h关于MAXMEMORY的配置!
妈也第一部分终于写完了。写了我俩天时间。有空继续写写下面的。如果有错漏啊啥的麻烦评论区见

你可能感兴趣的:(Redis,Redis,底层数据结构)