简单动态字符串
-
background
- SDS,simple dynamic string
- SDS是Redis的默认字符串表示
- 还可以作为缓冲区,AOF buffer,输入缓冲区
- C字符串只会用作字符串字面量用在无需对字符串值进行修改的地方
-
实现
struct sdshdr{ int len; int free; char buf[]; }
-
与C字符串的区别
-
获取长度 O(1)
-
杜绝缓冲区溢出
- C串的strcat要求desc后面为src预留了足够的memory,否则缓冲区溢出
- SDS的sdscat会先检查free,不满足的话拓展,然后再实际的修改操作
-
减少内存重分配次数
-
通过未使用空间实现了空间预分配和惰性空间释放
-
空间预分配
- 进行修改之后,len < 1 MB,分配使得 free = len
- 进行修改之后,len >= 1 MB,分配 free = 1 MB
-
惰性空间释放
- 并不立即使用内存重分配来回收缩短后多出来的字节,而是用free熟悉记录下来
- SDS提供API真正释放未使用空间
-
-
二进制安全
- C串只能保存文本,不能保存二进制数据,因为其以\0判断结尾
- SDS的 buf 称为字节数组,用len判断结尾
- redis用SDS不是保存字符,而是保存二进制数据
- 既可以保存文本,又可以保存二进制数据
-
兼容部分C字符串函数
- 使用\0结尾以适配
-
embstr
-
专门用来保存短字符串的一种优化编码方式
-
embstr编码与raw编码对应的字符串对象,都是由对象结构(redisObject)和数据结构(sdshdr)组成的
-
执行命令时产生的效果是相同的的
-
两者的区别
-
分配/释放 空间的两次 / 一次
用raw编码的字符串对象会调用两次内存分配函数来分别创建redisObject结构和sdshdr结构,而embstr编码则通过调用一次内存分配函数来分配一块连续的空间,空间中一次包含redisObject和sdshr两个结构
-
空间的 不连续 / 连续
embstr编码的对象比raw编码的对象能够更好的利用缓存带来的优势
-
链表
-
支持的功能
- 列表键,发布与订阅,慢查询,监视器,保存多个客户端的状态信息,构建客户端输出缓冲区
-
实现
typedef struct listNode{ struct listNode *prev; struct listNode *next; void *value; }listNode;
typedef struct list{ listNode *head; listNode *tail; unsigned int len; void *(*dup)(void *ptr); void *(*free)(void *ptr); void *(*match)(void *ptr, void *key); }list;
-
特性
-
双向、无环
-
带表头和表尾
-
带长度计数器
-
多态
- 通过为list设置类型特定的函数,可以使用list保存各种不同类型的值
-
字典
-
支持的功能
- Redis数据库,哈希键
-
底层实现
- 字典的底层实现是哈希表
- 一个哈希表有多个哈希表节点,每个哈希表节点保存来字典中的一个键值对
-
实现
-
哈希表
dict.h/dictht typedef struct dictht{ // 哈希表数组 dicEntry **table; // 哈希表数组的大小 unsigned long size; // 掩码,用于计算数组索引,= size - 1 unsigned long sizemask; // 已有节点的数量 unsigned long used; }dictht;
-
哈希表节点
typedef struct dictEntry{ // 键 void *key; // 值 union{ void *val; uint64_tu64; int64_ts64; }v; // 形成链表 struct dictEntry *next; }dictEntry;
-
字典
typedef struct dict{ // 类型特定函数 dictType *type; // 私有数据 void *privdata; // 哈希表 dictht ht[2]; // rehash索引 int trehashinx; }dict;
-
type和privdata是针对不同类型的键值对创建多态字典而设置的
-
type是dictType类型的指针,dictType保存了一系列用于操作特定类型键值对的函数
-
privdata保存了type的可选参数
-
-
-
哈希算法
- hash = dict->type->hashFunction(key);
- index = hash & dict->ht[x].sizemask;
- 使用链地址法解决键冲突,总是将新节点添加到表头
-
rehash
-
为ht[1]分配空间
-
拓展
- 新大小 = 大于等于ht[0].used * 2的最小的2的n次方
-
收缩
- 新大小 = 大于等于ht[0].used的最小的2的n次方
-
-
将ht[0]中所有键值对rehash到ht[1]上
- rehash = 重新计算哈希值和索引值
-
ht[1]设置为ht[0],ht[1]创建一个空白哈希表
-
-
拓展与收缩
-
负载因子 load factor
- used / size
-
拓展
- 取决于Redis服务器是否在BGSAVE或BGREWRITEAOF
- 分界线为 1 和 5
- 在上述两命令执行过程中,Redis需要创建子进程,OS采用写时复制,避免不必要的内存写入,节约内存
-
收缩
- 分界线为0.1
-
-
渐进式rehash
-
流程
- 分多次,渐进式地慢慢地rehash
- 为ht[1]分配空间,同时持有ht[0]和ht[1]
- 维护索引计数变量rehashidx,置0,表示rehash正式开始
- rehash期间,对字典对每次增删改查都会使ht[0]在rehashidx所有键值对rehash到ht[1],rehashidx++
- 随着字典操作的不断进行,所有键值对都会被rehash,rehashidx=-1,rehash操作完成
-
细节
- delete,find,update会在两个表上同时进行
- find,先在ht[0]上找,找不到再在ht[1]上找
- add只会在ht[1]上进行,ht[0]键值对只增不减
-
跳跃表
-
backgound
- Redis只在两个地方用到了跳表:有序集合键,集群节点的内部数据结构
-
定义
-
zskiplistNode
typedef struct zskiplistNode{ // 层 struct zskiplistLevel{ // 前向指针 struct zskiplistNode *forward; // 跨度 unsigned int span; }level[]; // 反向指针 struct zskiplistNode *backward; // 分值 double score; // 成员对象 robj *obj; }zskiplistNode;
-
level
-
forward
- 访问位于表尾方向的其他节点
-
span
- 跨度
-
-
backward
- 后退指针,从表尾向表头遍历时使用
-
score
- double类型,分值
-
obj
- 节点所保存的成员对象
-
-
zskiplist
typedef struct zskiplist{ // 表头和表尾 struct skiplistNode *header, *tail; // 表中节点的数量 unsigned long length; // 最大层数 int level; }zskiplist;
-
header
-
tail
-
level
- 跳跃表内除了表头外最大的层数
-
length
- 跳跃表除了表头包含节点的数量
-
-
细节
-
表头
- 计算length和level时不含
- 结构和其他节点一样,但是很多字段不会被用到
-
层中forward=null,则span=0
-
forward用于遍历,span用于计算排位
-
score相同时,按成员对象的字典序来排
-
各个节点保存的成员对象必须是唯一的
-
-
整数集合
-
backgroud
- intset
- 集合键的实现之一
- 一个集合只含有整数且数量不多时,Redis使用整数集合实现
-
实现
typedef struct intset{ // 编码方式 uint32_t encoding; // 集合包含的元素数量 uint32_t length; // 保存元素的数组 int8_t contents[]; }intset;
-
encoding
- contents真正的类型取决于encoding而不是int8_t
-
length
- 集合元素的数量 = contents数组的长度
-
contents[]
- 整数集合的每个元素都是content数组的一个数据项,数组中不包含任何相同的数据项
-
-
升级
-
何时升级:新元素添加到整数集合且类型比现有类型长
-
怎么升级:
- 扩容底层数组
- 从尾往头复制原有数组,注意有序性不变
- 添加新元素到底层数组的头或尾(想想为什么)
-
好处
-
提升灵活性
- 保存int16_t / int32_t / int64_t 到集合而不担心类型错误
-
节约内存
- 有需要时才升级
-
-
不能降级
-
压缩列表
-
background
- ziplist
- 集合键和哈希键的底层实现
- 列表键含少量列表项,小整数 / 短字符串时,Redis使用压缩列表
- 哈希键少量键值对,键和值都是小整数 / 短字符串时
-
压缩列表的构成
- 连续内存块组成短sequential数据结构
- 一个压缩列表含任意多个节点entry
- 每个entry保存一个字节数组或一个整数值
- zlbytes | zltail | zllen | entry1 | entry2 | entry3 | ****** | entryN | zlend
- zlbytes:整个压缩列表占的字节数,内存重分配或算zlend时使用
- zltail:zlbytes起始到表尾节点起始的字节数
- zllen:包含的节点数,值大于UINT16_MAX时需遍历列表才能得出节点数
- entryX:各个节点,长度不定
- zlend:标记压缩列表的末端
-
压缩列表节点的构成
-
previous_entry_length | encoding | content
-
previous_entry_length:压缩列表前一个节点的长度
-
encoding:记录content属性所保存数据的类型和长度
-
content:节点的值,可以是一个字节数组或整数
- 三种长度的字节数组 / 六种长度的整数值
-
-
连续更新
对象
s
-
Redis对象系统
- 字符串对象
- 列表对象
- 哈希对象
- 集合对象
- 有序集合对象
-
定义
typedef struct redisObject{ // 类型 unsigned type; // 编码 unsigned encoding; // 指向底层数据结构的指针 void *ptr; // 引用计数 int refcount; // 最后一次被访问的时间 unsigned lru; }robj;
-
type
- 上述五个中一个
-
encoding
- 对象所使用的编码 = 用了什么底层数据结构作为实现?
-
ptr
- 指向底层数据结构的指针
-
refcount
- 引用计数,用于垃圾回收,为0时释放内存
-
lru
- 对象最后一次被命令程序访问的时间
-
-
字符串对象
-
编码类型:int,raw,embstr
-
int
- 保存的是整数,整数值可以用long表示,void*转化为long来表示
-
raw
- 保存的是字符串值,长度>32字节,使用SDS编码
-
embstr
- 保存的是字符串值,长度 <= 32字节,使用embstr编码
-
存储能用long double表示的浮点数时会先将浮点数转换为字符串,遵循上述长度规则,执行特定操作时再转换回浮点型
-
编码的转换
-
对象保存的不再是整数值,而是一个字符串值
- int转换为raw
-
存储的整数值超过long的范围
- int转换为embstr
-
embstr编码的字符串被修改
只有int、raw编码的字符串对象可以被修改,所以embstr编码的字符串实际上是只读的
- embstr转换为raw
-
-
XMind - Trial Version