1、 String
Redis为了极致利用内存,为String类型的对象,设置了三种数据结构: int、embstr和raw。
- int : 在64位系统中,一个指针用8位字节来存储,正好是一个长整型的存储,所以为了节省空间,若一个字符串的内容可以转为long,那就将该字符串转为long类型存储,并将encoding设置为int, 可以节省空间,算是对长整型的一个优化
- raw : 如果保存的字符串大小大于32字节 ,则会使用一个简单动态字符串SDS来保存,并将字符串编码设置为raw
- embstr : 如果保存的字符串大小小于等于44字节 ,那么使用embstr来保存。
1.1 SDS结构
- 属性: len字符串长度、alloc预分配空间大小、flags sds类型、buf[] 存储字符串数组
1.2 SDS与C字符串的对比
- 长度计算: C中,字符串长度需要遍历计算,sds的话,自身便维护了len属性,可以直接读取
- 缓冲区溢出: c中,若未提前做好内存分配,会出现内存溢出的情况,SDS的话,会根据缓冲区大小和len大小,计算预留内存是否足够分配,并重新申请内存
- 动态扩展: SDS内存空间会在长度变化时,自动扩展计算。小于1M时翻倍,大于1M时,每次扩展1M
- 惰性释放: 字符串删除某些内容后所占用的内存空间并不会立即释放,后续字符串变更扩展就无需再申请内存。
1.3 embstr如何存放字符串
cpu读取数据时,通常会先读取到cache line中,一个缓存杭基本占64个字节,其中RedisObject最起码得占16个字节,所以读取一个RedisObject,会发现有48个字节浪费掉,所以为了提高内存利用率,在RedisObject后面又开辟了一个48字节的连续空间,将ptr的值放入其中。48个字节对应的时SDShdr8存储结构,而它在不存放任何数据的情况下,至少需要4个字节空间('\0'),所以还剩下44个字节来存放字符串,这也是为什么embstr大小要设置在44的原因。
2、 List
2.1 ziplist(为节省内存而设计的结构)
其中,三个头部数据: zlbyte表示整体占用的字节数,用于内存重分配或计算列表尾端; zltail为到达列表最后一个节点的偏移量,方便直接找到尾部节点;zllen为列表的节点数量。最后的zlend用来标记列表尾端,占用一个字节的大小。
列表中每个节点的存储方式:
typedef struct zlentry {
unsigned int prevrawlensize, prevrawlen; // prevrawlen是前一个节点的长度,prevrawlensize是指prevrawlen的大小,有1字节和5字节两种
unsigned int lensize, len; // len为当前节点长度 lensize为编码len所需的字节大小
unsigned int headersize; // 当前节点的header大小\
unsigned char encoding; // 节点的编码方式\
unsigned char *p; // 指向节点的指针\
} zlentry;
void zipEntry(unsigned char *p, zlentry *e) { // 根据节点指针返回一个enrty\
ZIP_DECODE_PREVLEN(p, e->prevrawlensize, e->prevrawlen); // 获取prevlen的值和长度\
ZIP_DECODE_LENGTH(p + e->prevrawlensize, e->encoding, e->lensize, e->len); // 获取当前节点的编码方式、长度等\
e->headersize = e->prevrawlensize + e->lensize; // 头大小\
e->p = p;\
}
作者:小罗老师\
链接:https://juejin.cn/post/6863256540439117831\
当添加的字符串长度超过默认的64的时候,由压缩链表转化为双向链表。
- zipList的缺点 : 连锁更新问题 在ziplist中,每个entry中都存储着前一个节点所占据的字节数,而这个数值又是一个变长编码。当前一个节点的大小发生变化的时候,后面都得跟着变化,每一次扩充都需要进行空间再分配操作。
一个压缩列表节点在保存上一个节点长度使用previous_entry_length属性,这个属性可以使用1字节或者是5字节。假设现有一个压缩列表里面保存的节点长度全部都是250-253,这时候previous_entry_length使用一字节记录就行。但是这时候添加一个新节点到头节点的位置,恰好这个节点的大小大于254字节,这时候所有后面字节都需要更新,因为他们的previous_entry_length都会变成5字节。(为什么是254??)
2.2 quickList
ziplist会引入频繁的内存申请和释放,而linkedlist由于指针也会造成内存的浪费,而且每个节点是单独存在的,会造成很多内存碎片,所以结合两个结构的特点,设计了quickList。
quickList 是一个 ziplist 组成的双向链表。每个节点使用 ziplist 来保存数据。本质上来说,quicklist 里面保存着一个一个小的 ziplist。结构如下:
其中ziplist太小,内存碎片变多,若ziplist过大,则分配大块连续内存空间的难度就偏大,所以quicklist将ziplist的大小设置为可变:4、16、32、64Kb……
3、 Set
整数集合有三个特点:全是数字、内容连续、有序但不重复
set集合底层实现为hash表或intset,主要介绍intset数据结构
typedef struct intset{
uint32_t encoding; 整数集合有三种编码方式: 16、32、64
uint32_t length; 整数集合中保存的元素个数
int8_t contents[]; 从小到大保存的整数集合中的元素
}intset;
4、 ZSet
zset 底层使用了两个数据结构:hash 和 跳跃链表。
typedef struct zskiplist{
//表头结点和尾节点
structz skiplistNode *heade,*tail;
//表中节点数量
unsigned long length;
//表中层数最大的节点的层数
int level;
}zskiplist;
typedof struct zskiplistNode{
//层
struct zskiplistNode{
struct zskiplistNode *forward; 前进指针
unsihned int span; 跨度
}level[];
struct zskiplistNode *backward; 后退指针
double score; 分值
robj *obj; 成员对象
}zsikplistNode;
5、 Hash
即 :Hash对象
有一个注意的点:rehash
- 字典的rehash当数据量过大时,扩容并不是一步完成的
- rehash过程中,新插入的数据放在hash表1中,并将原数据重新hash,计算到1中,读操作同时操作新旧两个hash表
- rehash过程中,使用dict中的rehashidx属性值
- rehash采用cow写时复制技术