Redis数据结构与对象
简单动态字符串
struct sdshdr{
// 记录buf已使用字节数量
int len;
// 记录buf未使用字节数量
int free;
char buf[];
}
-
常数复杂度获取长度
-
杜绝缓冲区溢出
-
减少修改字符带来内存重分配次数
- 空间预分配
- 惰性空间释放
-
二进制安全
-
兼容部分C字符串函数
链表
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 *ptr);
//节点值释放函数
void (*free)(void *ptr);
//节点值对比函数
int (*match)(void *ptr,void *key);
}list;
- 双端:链表节点带有prev和next指针,获取前置和后置节点的复杂度都是O(1)
- 无环:表头节点的prev指针和表尾节点的next指针都指向NULL
- 头尾指针:将程序获取头尾节点的复杂度降为O(1)
- 长度计数器:将程序获取表长的复杂度降为O(1)
- 多态:链表节点使用void*指针来保存节点值,并且可以通过list结构的
dup、free、match
为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值
字典
Redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,每个哈希表节点保存了字典中的一个键值对
typedef struct dictht{
//哈希表数组
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,用于计算索引值
//总是等于size-1
unsigned long sizemask;
//该哈希表已有节点的数量
unsigned long used;
}dictht;
typedef struct dictEntry{
//键
void *key;
//值
union{
void *val;
uint64_t u64;
int64_t s64;
} v;
//指向下个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
typedef struct dict{
//类型特定函数
dictType *type;
//私有数据
void *privdata;
//哈希表
dictht ht[2];
//rehash索引
//当rehash不在进行时,值为-1
int rehashidx;
} dict;
typedef struct dictType{
//计算哈希值的函数
unsigned int (*hashFunction)(const void *key);
//复制键的函数
void *(*keyDup)(void *privdata,const void *key)
...
}
哈希算法
- Redis计算哈希值方法:
hash=dict->type->hashFunction(key);
计算索引值的方法:index=hash & dict->ht[x].sizemask;
解决键冲突
- 当有两个或以上的键被分配到哈希表的同个索引,那么就发生了冲突
- Redis使用链地址法来解决冲突,被分配到索引的多个节点使用链表连接
- 为了提高速度,每次都是将新节点添加到链表的表头位置。
rehash
-
服务器目前没有在执行
BGSAVE
命令或BGREWRITEAOF
命令,并且哈希表的负载因子>=1服务器正在执行
BGSAVE
命令或BGREWRITEAOF
命令,并且哈希表的负载因子>=5 -
为字典ht[1]哈希表分配空间,大小取决于要执行的操作与ht[0]当前键值对的数量
将保存在ht[0]中的所有键值对存放到ht[1]指定的位置
当ht[0]的所有键值对都迁移完毕后,释放ht[0],并指向ht[1],并在ht[1]上创建一个空的哈希表,为下次rehash准备
跳跃表
Redis只有在两个地方用到了跳跃表,一个是实现有序集合键,另一个是在集群节点中作为内部数据结构。
typedef struct zskiplist{
//表头节点和表尾节点
structz zskiplistNode *header,* tail;
//表中节点的数量
unsigned long length;
//表中层数最大的节点的层数
int level;
} zskiplist;
typedef strct zskiplistNode{
//后退指针
struct zskiplistNode *backward;
//分值
double score;
//成员对象
robj *obj;
//层
struct zskiplistlevel{
//前进指针
struct zskiplistNode *forward;
//跨度
unsigned int span;
}level[];
} zskiplistNode;
整数集合
当一个集合只包含整数元素,并且元素不多时,Redis就会使用整数集合作为集合键的底层实现
typedef struct intset{
//编码方式
uint32_t encoding;
//集合包含的元素数量
uint32_t length;
//保存元素的数组
int8_t contents[];
} intset;
升级
- 根据新元素类型,扩展数组空间,为新元素分配空间
- 将底层数组现有所有元素都转为新元素相同类型,并将类型转换后的元素放到正确位置
- 将新元素添加到底层数组
整数集合不支持降低,一旦升级就不能降级
压缩列表
压缩列表是列表键和哈希键底层实现之一
当一个列表键只包含少量列表项,且每个列表项要么是小整数,要么是长度比较短的字符串,Redis就使用压缩列表来做列表键的底层实现
压缩列表节点的组成
- previous_entry_length记录压缩列表前一个节点的长度
- 前一个节点的长度<254字节时,该属性只有2位,且前一节点的长度就保存在这两位
- 前一个节点的长度>=254字节时,该属性有10位,且前两位表示这是一个5字节的长度,后8位表示前一个节点的长度
- encoding记录了节点的content属性所保存数据类型和长度。高两位表示存储的是字节数组还是整数。
- content存储节点的值。
连锁更新问题
对象
Redis没有直接使用前文的数据结构来实现键值对数据库,而是基于这些数据结构构建了一个对象系统,通过对象组织数据结构,包括字符串对象,列表对象,哈希对象,集合对象和有序集合对象这5种对象
typedef struct redisObject{
//类型
unsigned type :4;
//编码
unsigned encoding:4;
//指向底层实现数据结构的指针
void *ptr;
...
} robj;
字符串对象
-
如果字符串对象保存的是整数值,且这个数值可用long表示,底层就会以**
REDIS_ENCODING_INT
**编码来实现。 -
如果字符串对象是一个字符串值,且这个字符串长度**>32字节**,字符串将使用一个SDS保存,底层编码为**
REDIS_ENCODING_RAW
**。 -
如果字符串对象保存的是字符串,且这个字符串长度**<=32字节**,底层编码就是**
REDIS_ENCODING_EMBSTR
**,使用embstr编码的方式保存字符串。 -
embstr编码
- embstr编码则通过调用一次内存分配函数来分配一块连续空间,空间依次包括redisObject和sdshdr俩结构
-
编码的转换
-
int->raw
:对int编码的字符串对象执行后,保存的不再是整数值,而是字符串值时。比如整数追加字符串。 -
embstr->raw
:Redis没有为embstr编写修改程序,所以是只读的,当embstr编码的字符串修改后,就变成raw编码的字符串对象。
-
列表对象
-
当列表可以同时满足以下两个条件时,列表对象使用ziplist编码:
-
列表对象保存的所有字符串元素的长度都**<64字节**
-
列表对象保存的元素数量**<512个**
-
-
否则使用linkedlist编码
哈希对象
-
当哈希对象可以同时满足下两个条件时,使用ziplist编码:
- 哈希对象保存的所有键值对的值和键都<64字节
- 哈希对象保存的键值对数量**<512个**
-
否则使用hashtable编码
集合对象
-
当集合对象同时满足以下两个条件时,使用intset编码:
-
集合对象保存的所有元素都是整数值
-
集合对象保存的元素数量**<=512个**
-
-
否则使用hashtable编码
有序集合对象
-
当有序集合对象同时满足以下两条件时,对象使用ziplist编码:
- 有序集合保存的元素数量**<128个**
- 有序集合保存的所有元素成员的长度都<64字节
-
否则使用skiplist编码,使用一个zset结构(同时包含ziplist和skiplist)
-
typedef struct zset{ zskiplist *zsl; dict *dict; } zset;
-
dict字典为有序集合创建了一个从成员到分值的映射,字典中的每个键值对都保存了一个集合元素:键保存元素,值保存分值
-
通过字典以O(1)查找给定成员的分值。有序集合元素都是字符串对象,分值都是double类型浮点数
-
zset的跳跃表和字典通过指针来共享相同元素的成员和分值,不会浪费额外内存
-
类型检查的实现
-
类型检查的实现
- 为了确保只有制定类型的键可以执行某些特定命令,在执行前,Redis会先通过RedisObject的type属性检查输入键的类型是否正确
-
多态命令的实现
- 根据值对象的编码方式,选择正确的命令实现代码来执行
-
内存回收
- 由于C语言没有内存回收机制,Redis在对象系统中构建了引用计数器技术实现内存回收机制。每个对象的引用计数器信息由redisObject的refcount来记录。当对象的引用计数值为0时,所占用的内存会被释放。
-
对象共享
- 引用计数器还有共享对象的作用。如果两个不同键的值都一样(必须是整数值的字符串对象),则将数据库键的值指针指向一个现有的值对象,然后将被共享对象的引用计数加一。如果不是整数值的对象,则需要耗费大量的时间验证共享对象和目标对象是否相同,复杂度较高,消耗CPU时间,所以Redis不会共享包含字符串的对象。
- Redis在初始化服务时,会创建很多字符串对象,包含0~9999的整数(和Integer的常量池有点像),当需要时,就能直接复用
-
对象的空转时长
- redisObject还包含了lru属性,记录对象最后一个被命令程序访问的时间。
object idletime
命令可打印键的空转时长,就是当前时间减去lru时间计算得到的
- redisObject还包含了lru属性,记录对象最后一个被命令程序访问的时间。