redis 数据结构
SDS 简单动态字符串
- 基本结构
struct sdshdr{
//记录buf数组中已使用字节的数量
//记录SDS保存的字符串的长度
int len;
//记录buf数组中未使用字节的数量
int free;
//字节数组,保存字符串
char buf[];
}
- SDS与C字符串的区别
- 常数复杂度获得字符串长度 : int len
- 杜绝缓冲区溢出: C字符串不记录自身长度,SDS在进行修改的时候如果空间不满足所需的要求,API将自动扩展SDS空间
- 减少修改字符串时带来的的内存重分配次数
- C字符串每次增长或者缩短都要进行一次内存重分配(否则增可能导致内存溢出,缩可能导致内存泄露–>字符数组中的部分空间不被使用却没有回收)
- SDS使用空间预分配
- 小于1M时,程序分配和len属性同样大小的未使用空间,SDS的len==free值。例如修改之后len变成13字节,那会多分配13字节的未使用空间,buf实际长度变为13+13+1=27bytes
- 大于1M时,每次修改之后多分配1M,总内存同上
- 惰性空间释放SDS的API需要缩短SDS保存的字符串时,程序并不立即使用内存重分配来回收多余字节,而是只用free和len属性记录,并等待将来使用===>提高效率牺牲空间
- 二进制安全:因为SDS是通过len属性而不是空字符来判断字符串是否结束的
双向链表
- 除了链表键以外,发布与订阅,慢查询,监视器的等功能也用到了链表,redis服务器本身还用链表来保存多个客户端的状态信息,以及构建客户端的输出缓冲区
typedef struct listNode{
//前置节点
struct listNode * prev;
//后置节点
struct listNode * next;
//节点的值
void * value;
}listNode;
type struct list{
//表头结点
listNode * head;
//表尾节点
listNode * tail;
//链表所包含的节点数量
unsigned long len;
//节点值复制函数
void *(dup) (void *ptr);
//节点值释放函数
void *(free) (void *ptr);
//节点值对比函数
int *(match) (void *ptr,void *key);
}list;
- 双端,无环,带计数器,多态
- 多态:链表节点使用void*指针来保存节点值,通过list的三个属性的函数指针来指定类型特定的函数
- 多态:链表节点使用void*指针来保存节点值,通过list的三个属性的函数指针来指定类型特定的函数
字典
- 除了用来表示数据库之外,字典也是哈希键的底层实现之一(键值对比较多或者有许多元素比较长的字符串)
- 字典使用哈希表作为底层实现,每个哈希表节点保存着一个键值对
- dict的type和privdata属性是针对不同的键值对,为创建多态字典而设置的
- ht属性是一个包含两个项的数组,数组中的每个项都是一个哈希表,一般情况下只是用ht[0],rehash时会使用ht[1]
//哈希表结构
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_tu64;
int64_ts64;
}v;
//指向下一个哈希表节点,形成链表===>所谓拉链法或者链地址法解决哈希冲突
struct dictEntry *next;
}dictEntry;
---
// 字典结构
typedef struct dict{
//类型特定函数
dictType *type;
//私有数据
void *privdata;
//哈希表
dictht ht[2];
//rehash索引,当rehash不在进行时,值为-1
int rehashidx;
}dict;
- 哈希算法
当要将一个新的键值对添加到字典里面去的时候,程序需要根据键值对的值计算出哈希值和索引值,再根据索引值将包含新键值对的哈希节点放到哈希表数组的指定索引上面
hash = dict->type->hashFunction(key); //redis使用MurmurHash2算法计算键的哈希值
index = hash & dict->ht[x].sizemask;
- 链地址法解决哈希冲突
- rehash
- 满足条件>
- 服务器目前没有执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于1
- 服务器目前正在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于5
- 负载因子小于0.1会收缩哈希表
- 负载因子可以通过公式
load_factor = ht[0].used / ht[0].size
计算得出。(哈希表已保存节点数量/哈希表大小)
- 渐进式rehash:分多次、渐进的完成rehash—>rehash期间(rehashidx设置为0表示开始rehash)每次对字典执行crud操作,程序顺带将ht[0]哈希表在rehashidx索引上的所有键rehash到ht[1],完成后rehashidx增加1。ht[0]为空后将ht[1]设置为ht[0]。
- 在rehash期间对redis的rud操作会在两个哈希表上进行(先0后1),并且新增的键都会保存到ht[1],ht[0]不会进行任何添加操作(只减不增,最终空表)
跳跃表
- skiplist是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问多个节点的目的。 O(log n)
typedef struct zskiplistNode{
//层
struct zskiplistLevel{
// 前进指针
struct zskiplistNode *forword;
// 跨度
unsigned int span;
}leave[]; //一个节点可以有多个层级
//后退指针
struct zskiplistNode *backword;
//分值
double score; //分值相同按对象字典序排位
//成员对象
robj *obj;
}zskiplistNode;
- 层
- 跳跃表的每个层包含一个指向其他节点的指针,通过层来加快访问其他节点,层越多访问速度越快
- 每次新建一个node节点的时候,根据幂次定律(越大的数出现的概率越小)随机生成一个介于1—32之间的值作为level数组的大小—>节点的高度
- 跨度
实际上,遍历的操作只是用前进指针就够了,跨度只是用来计算排位的,在查找某个节点的过程中,将沿途访问过的所有层的跨度累计起来,得到的结果就是目标节点在跳跃表中的排位。
- 表头节点跟其他节点构造是一样的,只不过只用到了层这个对象成员。(有所有层)
- zskiplist结构包含属性
- header
- tail
- level:记录表内层数最大的节点层数(不计算表头的)
- length
整数集合
- 集合键的底层实现之一,当集合键只包含整数值元素,并且这个集合的元素不多
typedef struct intset{
//编码方式
uint32_t encoding;
//元素数量
uiny32_t length;
//元素数组
int_8 contents[];
}
- contents的类型由enconding属性决定(INTSET_ENC_INT16/32/64)
- 升级:当添加一个新元素并且类型比所有现有元素都要长的时候。升级之后引发升级的新元素大于所有现有元素则放最末尾,否则最开头。并且不支持降级
压缩列表
- ziplist是列表键和哈希键的底层实现之一。
- 压缩列表是redis为了节约内存而开发出来的,由一系列特殊编码的连续内存块组成的顺序数据结构。
zlibytes|zltail|zllen|entry1|entry2|...|entryN|zlend
- zlibytes 记录压缩列表占用的内存字节数
- zltail 列表表尾节点距离起始由多少字节,偏移量
- zlend 0xFF,标记表尾
- 节点entry
previous_entry_length|encoding|content
- previous_entry_length以字节为单位,记录前一个节点的长度。previous_entry_length属性的长度可以是1字节或者5字节(前一节点长度小于或大于等于254字节),5字节第一位设置为0xFE(254),后4字节保存前长
- encoding
- 连锁更新(书 7.3 节)多个连续的长度介于250-253字节的node,插入一个254字节在这些节点前面,引发连锁更新
redis 对象
- redis的对象系统实现了基于引用计数法的内存回收机制;redis还通过引用计数实现了对象共享机制,这一机制可以在适当的条件下,通过让多个数据库共享同一个对象来节约内存
- redis对象带有访问时间记录信息,可以计算数据库键的空转时长
string
- 字符串对象的编码可以是int,raw,embstr(大于或小于等于32字节)
hash
- 哈希对象的编码可以是ziplist或者hashtable
- ziplist编码的哈希对象,每当有新的键值对加入到哈希对象时,程序会先将保存了键的压缩列表节点推入到压缩列表尾,然后再将保存了值的压缩列表节点推入表尾,因而保存键值对的两个节点总是紧挨在一起。
- 当同时满足下面两个条件时使用ziplist:
- 哈希对象保存的所有字符串元素的长度都小于64字节
- 哈希对象保存的元素数量小于512个
list
- 列表对象的编码可以是ziplist或者linkedlist,当同时满足下面两个条件时使用ziplist:
- 对象列表保存的所有字符串元素的长度都小于64字节
- 列表对象保存的元素数量小于512个
set
- 集合对象的编码可以是intset或者hashtable,hashtable编码的每个键都是一个字符串,每个字符串对象包含了一个集合元素,而字典的值被全部设置为null
- 当同时满足下面两个条件时使用intset:
- 集合对象保存的所有元素都是整数值
- 集合对象保存的元素数量小于512个
zset
- 有序集合对象的编码可以是ziplist或者skiplist
- ziplist编码的zset对象,每个集合元素用两个紧挨的压缩列表节点来保存,第一个节点保存元素的成员,第二个节点保存元素的分值。
- skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表。
type struct zset{
zskiplist *zsl;
dict *dict;
}zset;
- zset结构中的zsl跳跃表按照分值从小到大保存了所有集合元素,每个跳表节点都保存了一个元素集合。其实单靠这个数据结构就可以实现zest了,单独使用字典也是可以的。zset结构中的dict字典为有序集合创建了一个从成员到分值的映射,key为成员,value为分值。通过这个字典,程序可以以O(1)的复杂度查找给定成员的分值。虽然zset结构通过跳表和字典来保存有序集合的元素,但是这两种数据结构都会通过指针来共享相同元素的成员跟分值,不会浪费额外的内存。
- 若:单独使用字典来实现有序集合,虽然可以O(1)的复杂度查找给定成员的分值,但是因为字典以无序的方式来保存元素,在每次执行范围性操作的时候(ZRANK,ZRANGE)程序都要对所有的元素进行排序O(N logN),以及额外的O(N)空间。
- 若:单独使用跳表,查找分值-> O(logN)
- 当有序集合满足下面两个条件的时候会使用ziplist编码
- 有序集合的元素数量小于128个
- 有序集合保存的所有元素成员的长度都小于64字节
对象共享
- 每个对象会有一个refcount属性,用来计算对象被引用的数量
- 目前来说,redis会在初始化的时候创建一万个字符串对象,包含从0-9999的所有整数。
- 为什么不使用共享的字符串类型的对象:对于字符串对象,验证操作的复杂度是O(N)。
空转时长
- LRU,记录对象最后一次被命令程序访问的时间