简单动态字符串
-
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