1 字典
:dict是一个用于维护key和value映射关系的数据结构。类似于java中的map,通过dictentry保存节点,其中dict同时保存两个entry数组;
typedef struct dict{
dictType *type; //dictType结构中包含自定义的函数,这些函数使得key和value能够存储任何类型的数据
void *privdata; //私有数据,保存着dictType结构中函数的 参数
dictht ht[2]; //只有在rehash的过程中,ht[0]和ht[1]才都有效。而在平常情况下,只有ht[0]有效,ht[1]里面没有任何数据。上图表示的就是重哈希进行到中间某一步时的情况。
long rehashidx; //rehash的标记,rehashidx == -1,表示没有进行 rehash
int itreators; //正在迭代的迭代器数量
}dict;
typedef struct dictht
{
//哈希表数组
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,用于计算索引值
unsigned long sizemask;
//该哈希已有节点的数量
unsigned long used;
}dictht;
//哈希表节点定义dictEntry结构表示,每个dictEntry结构都保存着一个键值对。
typedef struct dictEntry
{
//键
void *key;
//值
union{
void *val;
uint64_tu64;
int64_ts64;
}v;
// 指向下个哈希表节点,形成链表
struct dictEntry *next;
}dictEntry;
扩容的条件 在没进行AOF重写的时候 负载因子》1 或者在进行AOF重写的时候负载因子》5
渐进式rehash 过程:
- 新创建一个哈希表
ht[1]
,为新表分配空间,此时字典同时持有ht[0],ht[1]
两个哈希表 - 在字典中维持一个计数器变量
rehashidx
,初始值为0,表示开始rehash工作 - 在rehash期间,对字典的所有增删改查操作都会同时操作两个哈希表,目的是将
ht[0]
中的健值对重新hash到ht[1]
对应位置上,操作完成后rehashidx
加1 - 迁移完毕后,
rehashidx
设置成-1,表示rehash完成。释放ht[0]
,将ht[1]
设置成ht[0]
,并创建一个空哈希表,为下次rehash做准备。
为什么使用渐进式
redis的key 有很多,并且核心计算是单线程的,一次性散列那么多key会造成服务不可用。
2 sds(简单动态字符串)
redis是C语言实现,C语言中没有String 类型,而是用char数组进行拼接,所以redis设计了一种简单动态字符串(SDS[Simple Dynamic String])作为底实现
struct sdshdr {
//用于记录buf数组中使用的字节的数目,和SDS存储的字符串的长度相等
int len;
//用于记录buf数组中没有使用的字节的数目
int free;
//字节数组,用于储存字符串
char buf[]; //buf的大小等于len+free+1,其中多余的1个字节是用来存储’\0’的
};
DSD好处:在获取字符串长度的时候 可以直接获取len字段 时间复杂度为O(1);
在操作字符串的时候不用扩展char数组的空间大小 而sds有预分配和惰性释放
预决策:(还可以解决缓冲区溢出)
如果对SDS进行修改之后,SDS的长度(也即是len属性的值)将小于1MB,那么程序分配和len属性同样大小的未使用空间,这时SDS len属性的值将和free属性的值相同。
如果对SDS进行修改之后,SDS的长度(也即是len属性的值)将大于等于1MB,那么程序会分配1MB的未使用空间。
惰性释放
用于优化 SDS 的字符串缩短操作: 当 SDS 的 API 需要缩短 SDS 保存的字符串时, 程序并不立即使用内存重分配来回收缩短后多出来的字节, 而是使用 free 属性将这些字节的数量记录起来, 并等待将来使用。
还是二进制安全的,可以存储任意数据。
3.intset(整数集合)
当存储整数集合并且数据量较小的情况下Redis会使用intset作为set的底层实现,当数据量较大或者集合元素为字符串时则会使用dict实现set。
int8_t 的意思是 bit为 8位, 取值范围位-128—127
uint8_t 的意思是 bit 位 8位, 取值范围位0-255
其他一样
typedef struct intset {
uint32_t encoding; //intset的类型编码
uint32_t length; //集合包含的元素数量
int8_t contents[]; //保存元素的数组
}
inset数据集合具有以下特点:
intset将整数元素按顺序存储在数组里,并通过二分法降低查找元素的时间复杂度。
当新增的元素类型比原集合元素类型的长度要大时,可以进行元素升级,
元素升级的具体步骤:
- 1、根据新元素类型,扩展整数集合底层数组的大小,并为新元素分配空间。
- 2、将现有的元素都转成与新元素相同类型的元素
- 3、将新元素添加到整数集合中(保证有序)
4.skiplist(跳表)
跳跃表 是对有序链表的扩展,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问队尾目的。
skiplist查找效率很高,和红黑树相比,查找key的时间复杂度都为O(log n)但是红黑树的插入删除可能会引发旋转或者变色等操作 而跳表只需要改变相邻节点的指针。
一般来说 层级越多,访问节点的速度越快,但是跳表的层级完全是随机的。如果完全按照二分的思想层级固定,那么插入的时候就会打乱2:1的关系,维持2:1的关系的话 时间复杂度会降低到O(n);
5.ziplist(压缩表)
ziplist是一块连续的内存空间,元素之间紧挨着存储,没有任何冗余空隙的双向链表 它的设计目标就是为了提高存储效率。
struct ziplist<T> {
int32 zlbytes;
int32 zltail_offset;
int16 zllength;
T[] entries;
int8 zlend;
}
//zlbytes:ziplist的长度
//zltail_offset:最后一个元素距离压缩列表起始位置的偏移量,快速定位到最后一个节点,从而可以在ziplist尾部快速的执行push,pop操作
//zllength:元素个数 注意 zllength 只有16位,小于2的16次方-1的时候该值表示长度,如果超过需要遍历表得到长度
//entries:元素内容,挨个挨个紧凑存储
//zlend:标志压缩列表的结束,值恒为 0xFF(255)
每个entries节点由三部分组成:prevlength、encoding、data
- prevlengh: 记录上一个节点的长度,为了方便反向遍历ziplist
- encoding: 当前节点的编码规则
- data: 存储的数据
ziplist特点:
(1)内存空间连续:ziplist为了提高存储效率,从存储结构上看ziplist更像是一个表(list),但不是一个链表(linkedlist)。ziplist将每一项数据存放在前后连续的地址空间内,一个ziplist整体占用一大块内存。而普通的双向链表每一项都占用独立的一块内存,各项之间用指针连接,这样会带来大量内存碎片,而且指针也会占用额外内存。
(2)查询元素:查找指定的数据项就会性能变得很低,需要进行遍历整个zipList。
(3)插入和修改:每次插入或修改引发的重新分配内存(realloc)操作会有更大的概率造成内存拷贝,从而降低性能。跟list一样,一旦发生内存拷贝,内存拷贝的成本也相应增加,因为要拷贝更大的一块数据。
6.quicklist(快速列表)
quicklist双向链表是由多个节点(Node)组成,而quicklist的每个节点又是一个ziplist。
typedef struct quicklist {
quicklistNode *head; /* 头结点 */
quicklistNode *tail; /* 尾结点 */
unsigned long count; /* 在所有的ziplist中的entry总数 */
unsigned long len; /* quicklist节点总数 */
int fill : QL_FILL_BITS; /* 16位,每个节点的最大容量 */
unsigned int compress : QL_COMP_BITS; /* 16位,quicklist的压缩深度,0表示所有节点都不压缩,否则就表示从两端开始有多少个节点不压缩 */
unsigned int bookmark_count: QL_BM_BITS; /*4位,bookmarks数组的大小,bookmarks是一个可选字段,用来quicklist重新分配内存空间时使用,不使用时不占用空间*/
quicklistBookmark bookmarks[];
}
quicklist将 双向链表插入和修改元素不需要移动节点的优点 和 ziplist的存储效率很高优点(一整块连续内存)结合在一起,同时将各自的缺点进行一个折中的处理。
1 双向链表便于在表的进行插入和删除节点操作,但是它的内存开销比较大。首先,它在每个节点上除了要保存数据之外,还要额外保存两个指针;其次,双向链表的各个节点是单独的内存块,地址不连续,节点多了容易产生内存碎片。(内存开销大,额外的指针,单独的内存块,地址不连续,容易产生内存碎片)
2 ziplist由于是一整块连续内存,所以存储效率很高。但是,它不利于修改操作,每次数据变动都会引发一次内存的内存重新分配(realloc)。特别是当ziplist长度很长的时候,一次realloc可能会导致大批量的数据拷贝,进一步降低性能。
Redis还提供了一个配置参数list-max-ziplist-size,就是为了让使用者可以调整ziplist的长度。
list-max-ziplist-size -2
即可以取正值也可以取负值
//当取正值的时候,表示按照数据项个数来限定每个quicklist节点上的ziplist长度
//当取负值的时候,表示按照占用字节数来限定每个quicklist节点上的ziplist长度 此时表示每个quicklist节点上的ziplist大小不能超过8Kb。
当列表很长的时候,最容易被访问的很可能是两端的数据,中间的数据被访问的频率比较低 redis提供list-compress-depth来将中间的数据节点进行压缩,从而进一步节省空间
list-compress-depth 0
//0: 是个特殊值,表示都不压缩。这是Redis的默认值。
//1表示两端各有1个节点不压缩。
//2 、3 ....
参考 https://blog.csdn.net/Seky_fei/article/details/106968173