Redis基础知识大全

1 SDS

struct sdshdr{
    // 记录buf数组中已经使用的字节数量
    // 等于SDS所保存的字符串的长度
    int len;
    // 记录buf数组中未使用的字节数量
    int free;
    // 字节数组,用于保存字符串
    char buf[];
}

SDS为什么性能高

  • 常数复杂度获取字符串长度【O(1)】,C语言字符串需要遍历一次【O(N)】

  • 不用检查是否会溢出,SDS函数接口会帮我们检查是否会char数组溢出,如果溢出会申请一块新的更大的内存地址来保存字符串

  • 但是每次都重新分配空间,然后进行数组数据拷贝到新数组中,很耗费性能。所以像ArrayList一样,预先分配更多空间,SDS长度小于1MB的时候倍增,大于1MB的时候1MB的步进方式增加容量

  • 惰性释放空间,缩短字符串时,并不是立即重新分配字符串空间,直接修改’\0’的位置或者执行类似System.arraycopy(),而是使用free属性记录剩余空间大小

二进制安全

之前C语言字符串都是以\0来判断一个字符串是否结束,不管`\0之后还有没有可读的字符。

然而char buf[]是字节数组,而不是字符数组,判断一个字符串结束是以len属性来判断,而不是\0

2 链表

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 *prt);
    // 节点值释放函数
    void *(*free)(void *prt);
    // 节点值对比函数
    int *(*match)(void *prt, void *key);
}

list结构体用来持有链表的双端链表

  • dup()用于复制节点保存的值
  • free()用于释放节点保存的值
  • match()对比链表节点保存的值是否与另一个输入值相等

特性

  • 双端

  • 无环,头结点的pre,尾结点的next都指向null

  • 查看链表元素数量O(1)时间复杂度

  • 多态,链表节点使用void*,可以用于保存各种不同类型的值

3 哈希表

typedef struct dictht{
    // 哈希表数组
    dictEntry **table;
    // 哈希表大小,也就是table数组的长度
    unsigned long size;
    // 哈希表掩码值,用于计算索引值,总是等于size-1
    unsigned long sizemask;
    // 哈希表已有节点数量
    unsigned long used;
}

typedef struct dictEntry{
    // 键
    void *key;
    // 值【值可以是一个指针,也可以是uint64_t或者int64_t类型整数】
    union{
        void *val;
        uint64_t u64;
        int64_t s64;
    }
    // 指向下一个节点,形成链表,发生哈希冲突的时候使用
    struct dictEntry *next;
}

dictEntry **table;是一个指针数组,所以使用了两个*。size也就是table数组的长度

K-V键值对,K唯一

在这里插入图片描述

应用—字典的实现

typedef struct dict {
    // 类型特定函数结构体,保存了一簇用于操作特定类型键值对的函数, Redis 会为用途不同的字典设置不同的类型特定函数。
    dictType *type;
    // 私有数据,privdata 属性则保存了需要传给类型特定函数的可选参数。
    void *privdata;
    // 哈希表
    dictht ht[2];
    // rehash 索引
    // 当 rehash 不在进行时,值为 -1
    int rehashidx; /* rehashing not in progress if rehashidx == -1 */

} dict;

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 属性是一个包含两个项的数组, 数组中的每个项都是一个 dictht 哈希表, 一般字典只使用 ht[0] 哈希表, ht[1] 哈希表只会在对 ht[0] 哈希表进行 rehash 时使用。ht 相当于是一个滚动数组,当哈希表扩容或者缩小的时候都会进行rehash操作,移动顺序就 0 -> 1,1 -> 0dictType *type;类型特定函数结构体,保存了一簇用于操作特定类型键值对的函数

在这里插入图片描述

字典哈希算法

如果我们要将一个键值对 k0v0 添加到字典里面, 那么程序会先使用语句:

hash = dict->type->hashFunction(k0);

dictType *type;类型特定函数结构体,保存了一簇用于操作特定类型键值对的函数

计算键 k0 的哈希值。

假设计算得出的哈希值为 8 , 那么程序会继续使用语句:

index = hash & dict->ht[0].sizemask = 8 & 3 = 0;

计算出键 k0 的索引值 0 , 这表示包含键值对 k0v0 的节点应该被放置到哈希表数组的索引 0 位置上

字典rehash

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

  1. 为字典的 ht[1]哈希表分配空间, 这个哈希表的空间大小取决于要执行的操作, 以及 ht[0]当前包含的键值对数量 (也即是ht[0].used属性的值):

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

    • 如果执行的是 收缩操作, 那么 ht[1] 的大小为第一个大于等于 ht[0].used 的2^n

  2. 将保存在 ht[0] 中的所有键值对 rehash 到 ht[1] 上面: rehash 指的是重新计算键的哈希值和索引值, 然后将键值对放置到 ht[1] 哈希表的指定位置上。

  3. ht[0] 包含的所有键值对都迁移到了 ht[1] 之后, 释放 ht[0] , 将 ht[1] 设置为 ht[0] , 并在 ht[1] 新创建一个空白哈希表, 为下一次 rehash 做准备。

渐进式 rehash

如果哈希表里中数据量巨大, 那么要一次性将这些键值对全部 rehash 到 ht[1] 的话,可能会导致服务器在一段时间内停止服务。所以要分多次、渐进式地完成rehash。

  1. ht[1] 分配空间, 让字典同时持有 ht[0]ht[1] 两个哈希表。
  2. 在字典中维持一个索引计数器变量rehashidx , 并将它的值设置为 0 , 表示 rehash 工作正式开始
  3. 在 rehash 进行期间, 每次对字典CRUD操作时, 还会顺带ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] , 当 0号槽rehash 工作完成之后, 将 rehashidx 属性的值增一
  4. 最终 ht[0] 的所有键值对都会被 rehash 至 ht[1] , 这时**将 rehashidx 属性的值设为 -1,表示 rehash 操作已完成 **

4 跳跃表

在这里插入图片描述

持有跳表的数据结构

  • header :指向跳跃表的表头节点。
  • tail :指向跳跃表的表尾节点。
  • level :记录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不计算在内)。
  • length :记录跳跃表的长度,也即是,跳跃表目前包含节点的数量(表头节点不计算在内)。

跳表节点

typedef struct zskiplistNode {
    // 后退指针
    struct zskiplistNode *backward;
    // 分值
    double score;
    // 成员对象
    robj *obj;
    // 层
    struct zskiplistLevel {
        // 前进指针
        struct zskiplistNode *forward;
        // 跨度
        unsigned int span;
    } level[];
} zskiplistNode;
  • score属性是一个double类型,跳跃表中的所有节点都按分值从小到大来排序
  • obj属性代表成员对象,是一个指针,指向一个字符串对象,而字符串对象则保存着一个SDS值。
  • backward 属性是后退指针,用于从表尾向表头方向访问节点:每次只能后退至前一个节点
  • level数组可以包含多个元素,每个元素都包含一个指向其他节点的指针,可以通过这些层来加快访问其他节点的速度
    • level[i].forward属性,用于从表头向表尾方向访问节点。
    • level[i].span属性,用于记录两个节点之间的距离:跨度用来计算排位(rank)的:在查找某个节点的过程中, 将沿途访问过的所有层的跨度累计起来, 得到的结果就是目标节点在跳跃表中的排位

5 整数集合

typedef struct intset {
    // 编码方式决定存储的是int16_t 、 int32_t 或者 int64_t
    uint32_t encoding;
    // 集合包含的元素数量
    uint32_t length;
    // 保存元素的数组
    int8_t contents[];
} intset;

升级(不支持降级)

假设现在有一个整数集合, 集合中包含三个 int16_t 类型的元素

在这里插入图片描述

每个元素都占用 16 位空间, 所以整数集合底层数组的大小为 3 * 16 = 48

在这里插入图片描述

int32_t 整数值 65535 添加到整数集合, int32_t 比整数集合当前所有元素的类型都要长,需要先对整数集合进行升级。

每个 int32_t 整数值需要占用 32 位空间,空间重分配之后, 底层数组的大小将是 32 * 4 = 128

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

最终变成

在这里插入图片描述

6 压缩列表

在这里插入图片描述

  • 列表 zlbytes 属性表示压缩列表的总字节长。
  • 列表 zltail 属性, 如果我们有一个指向压缩列表起始地址的指针 p , 那么只要用指针 pzltail 属性 , 就可以计算出表尾节点的地址。
  • 列表 zllen 属性表示压缩列表包含节点。

每个压缩列表节点都由previous_entry_lengthencodingcontent 三个部分组成

如果我们有一个指向当前节点起始地址的指针 c , 用指针 c 减去当前节点 previous_entry_length 属性值, 就可以得出一个指向前一个节点起始地址的指针。

previous_entry_length

previous_entry_length 属性的长度可以是 1 字节或者 5 字节:

  • 如果前一节点的长度小于 254 字节, 那么 previous_entry_length 属性的长度为 1 字节: 前一节点的长度就保存在这一个字节里面。
  • 如果前一节点的长度大于等于 254 字节, 那么 previous_entry_length 属性的长度为 5 字节: 其中属性的第一字节会被设置为 0xFE(十进制值 254), 而之后的四个字节则用于保存前一节点的长度。

压缩列表的从表尾向表头遍历操作就是使用这一原理实现的: 只要我们拥有了一个指向某个节点起始地址的指针, 那么通过这个指针以及这个节点的 previous_entry_length 属性, 程序就可以一直向前一个节点回溯, 最终到达压缩列表的表头节点。

encoding

记录了节点的 content 属性所保存数据的类型以及长度

10开头五个字节ByteArray

00开头一个字节ByteArray

01开头两个字节ByteArray

在这里插入图片描述

content

负责保存节点的值, 节点可以保存一个ByteArray或者一个整数值, 值的类型和长度由节点的 encoding 属性决定。

连锁更新

后一个节点NodeNextprevious_entry_length 属性记录着前一个节点NodePre的长度,

NodePre增长导致NodeNext的属性值也要增加,如果NodeNextpre...Len增加过程中超过254,自己也要发生相应变化

7 五种对象

typedef struct redisObject {
    // 类型
    unsigned type:4;
    // 编码
    unsigned encoding:4;
    // 指向底层实现数据结构的指针
    void *ptr;
    // 引用计数
    int refcount;
    // 对象最后一次被命令程序访问的时间
	unsigned lru:22;
} robj;

type类型一览

对象对象 type 属性的值TYPE 命令的输出
字符串对象REDIS_STRING"string"
列表对象REDIS_LIST"list"
哈希对象REDIS_HASH"hash"
集合对象REDIS_SET"set"
有序集合对象REDIS_ZSET"zset"

编码和底层实现

ptr 指针指向对象的底层实现数据结构,encoding 属性记录了对象所使用的编码

encoding编码常量编码所对应的底层数据结构
REDIS_ENCODING_INTlong 类型的整数
REDIS_ENCODING_EMBSTRembstr 编码的简单动态字符串
REDIS_ENCODING_RAW简单动态字符串
REDIS_ENCODING_HT字典
REDIS_ENCODING_LINKEDLIST双端链表
REDIS_ENCODING_ZIPLIST压缩列表
REDIS_ENCODING_INTSET整数集合
REDIS_ENCODING_SKIPLIST跳跃表和字典

每种类型的对象都至少使用了两种不同的编码

对象 | String : Int + embStr + raw | 编码

对象 | list : 压缩列表 + 链表 | 编码

对象 | hash :压缩列表 + 字典 | 编码

对象 | set :整数集合 + 字典 | 编码

对象 | zset:压缩列表 + 跳表 | 编码

8 字符串对象

**String : Int + embStr + raw **

在这里插入图片描述

一个字符串对象保存的是整数值, 并且这个整数值可以用 long 类型来表示, 那么字符串对象会将整数值保存在字符串对象结构的 ptr属性里面(将 void* 转换成 long ), 并将字符串对象的编码设置为 int

如果字符串对象保存的是一个字符串值, 并且这个字符串值的长度大于 39 字节 , 那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串值, 并将对象的编码设置为 raw 。字符串值的长度小于等于 39 字节, 那么字符串对象将使用 embstr 编码的方式来保存这个字符串值。

  • embst 所需的内存分配次数一次,raw 两次,分别为redisObjectsdshdr分配两次内存

  • 释放 embstr 只需要调用一次内存释放函数, 而释放 raw 需要调用两次内存释放函数

  • 因为 embstr 的所有数据都保存在一块连续的内存里面, 比起 raw 能够更好地利用缓存 带来的优势。

int embstr 满足条件的情况下会转换成 raw

9 列表对象

**list : 压缩列表 + 链表 **

redis> RPUSH numbers 1 "three" 5
(integer) 3

压缩列表形式:

在这里插入图片描述

链表形式:

在这里插入图片描述

StringObject 对象的真正格式 ↓

在这里插入图片描述

编码格式转化:

当列表对象可以同时满足以下两个条件时, 列表对象使用 ziplist 编码:

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

不能满足这两个条件的列表对象需要使用 linkedlist 编码。

10 哈希对象

hash :压缩列表 + 字典

压缩列表

  • 保存了同一键值对的两个节点总是紧挨在一起, 保存键的节点在前, 保存值的节点在后;
  • 先添加到哈希对象中的键值对会被放在压缩列表的表头方向, 而后来添加到哈希对象中的键值对会被放在压缩列表的表尾方向。

字典

  • 字典的每个键都是一个字符串对象, 对象中保存了键值对的键;
  • 字典的每个值都是一个字符串对象, 对象中保存了键值对的值。

在这里插入图片描述

编码转换

当哈希对象可以同时满足以下两个条件时, 哈希对象使用 ziplist 编码:

  1. 哈希对象保存的所有键值对的键和值的字符串长度都小于 64 字节
  2. 哈希对象保存的键值对数量小于 512

不能满足这两个条件的哈希对象需要使用 hashtable 编码。

11 集合对象

set :整数集合 + 字典

redis> SADD numbers 1 3 5
(integer) 3

整数集合

在这里插入图片描述

字典

hashtable 作为底层实现时字典的每个键都是一个字符串对象, 每个字符串对象包含了一个集合中的元素, 而**字典的值则全部被设置为 NULL **

在这里插入图片描述

编码的转换

当集合对象可以同时满足以下两个条件时, 对象使用 intset 编码:

  1. 集合对象保存的所有元素都是整数值
  2. 集合对象保存的元素数量不超过 512

不能满足这两个条件的集合对象需要使用 hashtable 编码。

12 有序集合对象

zset:压缩列表 + 跳表

压缩列表

每个集合元素使用两个紧挨在一起的压缩列表节点来保存, 第一个节点保存元素的成员, 而第二个元素则保存元素的分值。

分值较小的元素被放置在靠近表头的方向, 而分值较大的元素则被放置在靠近表尾的方向

redis> ZADD price 8.5 apple 5.0 banana 6.0 cherry
(integer) 3

在这里插入图片描述

跳表

zsl跳跃表按分值从小到大保存了所有集合元素

字典为有序集合创建了一个从成员到分值的映射。可以O(1)时间复杂度内获得成员的分值。不用O(logn)遍历跳表了。

typedef struct zset {
	//zsl 跳跃表按分值从小到大保存了所有集合元素
    zskiplist *zsl;
	// 字典为有序集合创建了一个从成员到分值的映射
    dict *dict;

} zset;

在这里插入图片描述

在这里插入图片描述

编码的转换

当有序集合对象可以同时满足以下两个条件时, 对象使用 ziplist 编码:

  1. 有序集合保存的元素数量小于 128
  2. 有序集合保存的所有元素成员的长度都小于 64 字节

13 内存回收

typedef struct redisObject {
    // 类型
    unsigned type:4;
    // 编码
    unsigned encoding:4;
    // 指向底层实现数据结构的指针
    void *ptr;
    // 引用计数
    int refcount;
    // 对象最后一次被命令程序访问的时间
	unsigned lru:22;
} robj;

应该就像是Netty中预先申请一大块内存自己管理一样,Redis肯定也会因为压榨性能而自己预先申请一大块内存。

既然申请了就得自己好好管理,使用引用计数器 refcount

  • 在创建一个新对象时, 引用计数的值会被初始化为 1
  • 当对象被一个客户端使用时, 它的引用计数值会被增一;
  • 当对象不再被一个程序使用时, 它的引用计数值会被减一;
  • 当对象的引用计数值变为 0 时, 对象所占用的内存会被释放。
// 创建一个字符串对象 s ,对象的引用计数为 1
robj *s = createStringObject(...)

// 对象 s 执行各种操作 ...

// 将对象 s 的引用计数减一,使得对象的引用计数变为 0
// 导致对象 s 被释放
decrRefCount(s)

14 对象共享

A 创建了一个包含整数值 100 的字符串对象, B 也要创建一个同样保存了整数值 100

可以让键 A 和键 B 共享同一个字符串对象

多个键共享同一个值对象需要执行以下两个步骤:

  1. 将数据库键的值指针指向一个现有的值对象;
  2. 将被共享的值对象的引用计数增一。

15 对象的空转时长

typedef struct redisObject {
    // 类型
    unsigned type:4;
    // 编码
    unsigned encoding:4;
    // 指向底层实现数据结构的指针
    void *ptr;
    // 引用计数
    int refcount;
    // *******对象最后一次被命令程序访问的时间*******
	unsigned lru:22;
} robj;

如果服务器打开了 maxmemory 选项, 并且服务器用于回收内存的算法为 volatile-lru 或者 allkeys-lru , 那么当服务器占用的内存数超过了 maxmemory 选项所设置的上限值时, 空转时长较高的那部分键会优先被服务器释放, 从而回收内存。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值