redis的设计原理

1、字典

字典是Redis数据库以及HashTable编码方式的底层实现。一个redisDB就是一个字典;默认是16个redisDB
字典的底层使用散列(数组hashtable),同时使用链地址法的方式解决散列冲突,那么最终就是指针数组的形式,数组中的每个元素都是一个指向DictEntry的指针,这里的hashtable类似于hashmap的扩容方式;通过key进行hash后得出所在数组的位置,然后进行存储,如果出现hash冲突,则是尾插法进行存储;
在这里插入图片描述
1)、Redis使用dictht结构来表示散列表
typedef struct dictht {
dictEntry **table;
unsigned long size;
unsigned long sizemask;
unsigned long used;
}dictht;
table指针:指向散列表的地址。

size属性:存储散列表的大小。

sizemask属性:用于计算索引值。

used属性:散列表中节点的个数。

2)、Redis使用dictEntry结构来表示散列表中的节点
typedef struct dictEntry {
void *key;
union{
void val;
uint_tu64;
int64_ts64;
}v
struct dictEntry next
;
}dictEntry;
key指针:指向Key对象。
value属性:可以是指向Value对象(指针)、uint64_t整数、int64_t整数。
next指针:指向下一个DictEntry。

3)、Redis使用dict结构来表示字典,每个字典中包含两个dictht。
typedef struct dict{
dictType *type;
void *privatedata;
dictht ht[2];
int rehashidx;
}dict;

type指针:指向DictType,DictType定义了一系列函数。
privatadata属性:传给特定函数的可选参数。
ht数组:长度为2的dictht数组,一般情况下只会使用ht[0]散列表,ht[1]散列表只会在对ht[0]散列表进行rehash时使用。
rehashidx属性:记录rehash的进度,如果目前没有进行rehash那么值为-1。

dictType的结构(定义了一系列函数):
typedef struct dictType{
unsigned int (*hashFunction)(const void *key); // H(K)散列函数
void *(*keyDup)(void *privatedata, const void *key); // 复制Key
void *(*valDup)(void *privatedata, const void *obj); // 复制Value
int (*keyCompare)(void *privatdata, const void *key1 , const void *key2); // 对比Key
void (*keyDestructor)(void *privatedata, void *key); // 销毁Key
void (*valDestructor)(void *privatedata, void *obj); // 销毁Value
}dictType;

DictEntry和RedisObject:
Redis中的每个Key-Value在内存中都会被划分成DictEntry以及代表Key和Value的对象(redisObject
)。
DictEntry包含分别指向Key和Value对象的指针以及指向下一个DictEntry的指针。
Redis使用RedisObject来表示对象,由于Key固定是字符串类型,因此使用字符串对象来表示,Value可以是字符串、列表、哈希、集合、有序集合对象中的一种。

在这里插入图片描述

1)、Redis使用redisObject结构来表示对象(存储对象的相关信息):
typedef struct redisObject {
unsigned type;
unsigned encoding;
unsigned lru;
int refcount;
void *ptr;
}robj;
type属性:存储对象的类型(String、List、Hash、Set、ZSet中的一种)
encoding属性:存储对象使用的编码方式,不同的编码方式使用不同的数据结构进行存储。
lru属性:存储对象最后一次被访问的时间。
refcount属性:存储对象被引用的次数。

*ptr指针:指向对象的地址。
使用type命令可以查看对象的类型。
使用object encoding命令可以查看对象使用的编码方式。
使用object idletime命令可以查看对象的空闲时间(即多久没有被访问)
使用object refcount命令可以查看对象被引用的次数。
这些命令都是通过Key找到对应的DictEntry,再从DictEntry的value指针所指的RedisObject中进行获取。

2、在字典中进行查找、添加、更新、删除操作

2.1、在字典中进行查找
以客户端传递的Key作为关键字K,通过dict中的dictType的H(K)散列函数计算散列值,使用dictht[0]的sizemask属性和散列值计算索引,然后遍历索引对应的链表,如果存在Key相同的DictEntry则直接返回,否则返回NULL。
2.2、在字典中进行添加和更新
以客户端传递的Key作为关键字K,通过dict中的dictType的H(K)散列函数计算散列值,使用dictht[0]的sizemask属性和散列值计算索引,然后遍历索引对应的链表,如果存在Key相同的DictEntry则进行更新,否则创建代表Key和Value的对象,然后创建一个DictEntry并使其分别指向Key和Value的对象,最终将该DictEntry追加到链表的末尾。
2.3、在字典中进行删除(查到后进行删除)
以客户端传递的Key作为关键字K,通过dict中的dictType的H(K)散列函数计算散列值,使用dictht[0]的sizemask属性和散列值计算索引,然后遍历索引对应的链表,如果存在Key相同的DictEntry则进行删除。

3、散列表的扩容和缩容

由于散列表的负载因子需要维持在一个合理的范围内,因此当散列表中的元素过多时会进行扩容,过少时会进行缩容。
一旦散列表的长度发生改变,那么就要进行rehash,即对原先散列表中的元素在新的散列表中重新进行hash。
Redis中的rehash是渐进式的,并不是一次性完成,因为出于性能的考虑,如果散列表中包含上百万个节点,如果一次性完成rehash的话,那么有可能导致Redis在一定时间内无法正常对外提供服务。
在rehash进行期间,每次对字典执行查找、添加、更新、删除操作时,除了会执行相应的操作以外,还会顺带的将ht[0]散列表在rehashidx索引上的所有节点rehash到ht[1]上,然后将rehashidx属性的值加1。
渐进式Rehash的步骤
1.为字典的ht[1]散列表分配空间。
若执行的是扩容操作,那么ht[1]的长度为一个大于等于ht[0].used2的2ⁿ。 比如ht[0].used=7使用的数组位置,那么ht[0].used*2=14,必须满足2n的条件,所以ht[0].used*2=24=16;
*若执行的是缩容操作,那么ht[1]的长度为一个大于等于ht[0].used的2ⁿ。
2.rehashidx属性设置为0,表示开始进行rehash。
3.在rehash进行期间,每次对字典执行查找、添加、更新、删除操作时,除了会执行相应的操作以外,还会顺带将ht[0]散列表在rehashidx索引上的所有节点rehash到ht[1]上,然后将rehashidx属性的值加1。
4.随着对字典的不断操作,最终在某个时刻,ht[0]散列表中的所有节点都会被rehash到ht[1]上,此时将rehashidx属性设置为-1,表示rehash已结束。
*在进行渐进式rehash的过程中,字典会同时使用ht[0]和ht[1]两个散列表,因此字典的查找、更新、删除操作会在两个散列表中进行,如果在ht[0]计算得到的索引指向NULL则从ht[1]中进行查找。

A.String字符串:

redisObject对象中void *ptr;属性指向以SDS类型存储的字符串地址(SDS是redis自己的字符串类型):
在这里插入图片描述
len属性:存储字符串的长度。
free属性:存储字节数组中未使用的字节数量。
buf[]属性:字节数组,用于存储字符。
字节数组中会有\0结束符(这个C语言的语法),该结束符不会记录在len属性中。
SDS相比C语言的字符串:
C语言中存储字符串的字节数组其长度总是N+1(最后一个是结束符),当对字符串进行增长和缩短操作时需要使用内存重分配来重新为对象分配内存。
为了减少内存重分配的次数,Redis自定义了字符串对象(sdshdr),通过未使用的空间解除了字符串长度与底层数组长度之间的关系,在SDS中buf数组的长度不一定等于字符串的长度+1,数组里面还可以包含未使用的字节。
通过未使用的空间,SDS实现了空间预分配和惰性空间释放两种策略,从而减少由于字符串的修改导致内存重分配的次数。
空间预分配:当需要对SDS保存的字符串进行增长操作时,程序除了会为SDS分配所必须的空间以外,还会为SDS分配额外的未使用的空间。
惰性空间释放:当需要对SDS保存的字符串进行缩短操作时,程序并不会立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些多出来的字节数量记录出来,等待将来使用。

Redis提供的编码方式

Redis提供了八种编码方式,每种编码方式都有其特定的数据结构。
redis_encoding_int // 整数字符串
redis_encoding_embstr // 短字符串
redis_encoding_row // 长字符串
redis_encoding_ziplist // 压缩列表
redis_encoding_linkedlist // 链表
redis_encoding_intset // 整数集合
redis_encoding_hashtable // hashTable
redis_encoding_skiplist // 跳跃表

1、INT编码方式
在这里插入图片描述
INT编码方式会将RedisObject中的*ptr指针直接改写成long ptr,ptr属性直接存储字面量,也就是ptr直接表示value整数而不是指针;
存储的对象:
字符串: 如果字符串的值是整数,同时可以使用long来进行表示,那么Redis将会使用INT编码方式。

2、EMBSTR编码方式
在这里插入图片描述
ptr属性指向SDS对象;

3、ROW编码方式
在这里插入图片描述
EMBSTR和ROW编码方式在内存中都会创建字符串对象(SDS),区别在于EMBSTR编码方式中RedisObject和SDS共同使用同一块内存单元,Redis内存分配器只需要分配一次内存,而ROW编码方式中需要单独的为RedisObject和SDS分配内存单元。

4.ZIPLIST编码方式
压缩列表是Redis为了节约内存而开发的,它是一块顺序表(顺序存储结构,内存空间连续),一个压缩列表中可以包含多个entry节点,每个entry节点可以保存一个整数值或者字符串
在这里插入图片描述
zlbytes:记录了压缩列表的大小(占4个字节)
zltail:记录了压缩列表最后一个节点距离起始位置的大小(占4个字节)
zllen:记录了压缩列节点的个数(占2个字节)
entry:压缩列表中的节点(大小由节点中存储的内容所决定)
zlend:压缩列表的结束标志(占1个字节)
如果存在一个指针P指向压缩列表的起始位置,就可以根据P+zltail得到最后一个节点的地址。
在这里插入图片描述
5、LINKEDLIST编码方式

在这里插入图片描述
Redis使用listNode结构来表示链表中的节点。
typedef struct listNode {
struct listNode *prev;
struct listNode *next;
void *value;
}listNode;
每个listNode节点分别包含指向前驱和后继节点的指针以及指向元素的指针。

Redis使用list结构来持有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;
head指针:指向链表的头节点。
tail指针:指向链表的最后一个节点。
len属性:存储链表中节点的个数

6、INTSET编码方式
在这里插入图片描述
Redis使用intset结构来表示整数集合。
typedef struct inset {
uint32_t encoding;
uint32_t length;
int8_t contents[];
}intset;
encoding属性:标识contents数组的类型,支持INTESET_ENC_INT16、INTESET_ENC_INT32、INTESET_ENC_INT64。
length属性:存储整数集合中元素的个数。
contents数组:存储整数集合中的元素(从小到大进行排序,并且保证元素不会重复)
Contents升级
当往整数集合中添加一个比当前Contents数组类型还要大的元素时,将要进行Contents的升级。
1.对Contents数组进行扩容( (length + 1) * 新类型的大小)
2.将原有的元素转换成与新元素相同的类型,然后移动到正确的位置上。
3.将新元素添加到数组当中。
4.将encoding属性修改为新元素的类型。
*contents数组不支持降级,一旦对contents数组进行了升级那么就要一直保持升级后的状态。

7.HASHTABLE编码方式
在这里插入图片描述
8.SKIPLIST编码方式(跳表)
在这里插入图片描述
中文名是跳表的存储方式:
跳表(一般是链表的数据结构)就是建立索引层,从最高层开始查找,定位到下一层,知道找到最后一层,调表的好出可以减少查询的复杂度,如果不使用调表,则会挨个遍历,复杂度为O(n),如果使用调表复杂度可以看作是O(logN);

每次创建一个新的跳跃表节点时,会随机生成一个介于1到32之间的值作为level数组的大小。
Redis使用zskiplistNode结构来表示跳跃表中的节点.
typedef struct zskiplistNode {
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned int span;
}level[];
struct zskiplistNode *backward;
double score;
robj *obj;
}zskiplistNode
level[]数组:用于存储zskiplistLevel,每个zskiplistLevel都包含forward和span属性,其中forward属性用于指向表尾方向的其他节点,而span属性则记录了forward指针所指向的节点距离当前节点的跨度(forward指针遵循同层连接的原则)

backward属性:指向上一个节点的指针。
score属性:存储元素的分数。
obj指针:指向元素的地址(字符串对象)

Redis使用zskiplist结构来持有zskiplistNode
typedef struct zskiplist {
struct zskiplistNode *header,*tail;
unsigned long length;
int level;
}zskiplist;
header指针:指向跳跃表的头节点。
tail指针:指向跳跃表的最后一个节点。
length属性:存储跳跃表中节点的个数(不包括表头节点)
level属性:跳跃表中节点level的最大值(不包括表头节点)

遍历zskiplist的流程
1.通过zskiplist的header指针访问跳跃表中的头节点。
2.从下一个节点最高的level开始往下遍历,若下一个节点的最高level比当前节点的最高level要大,则从当前节点的最高level开始往下遍历。
3.当不存在下一个节点时,遍历结束。

Redis中的对象

Redis中一共包含五种对象,分别是字符串对象、列表对象、哈希对象、集合对象、有序集合对象,每种对象都支持多种编码方式,不同的编码方式之间使用不同的数据结构进行存储。

Redis各个对象支持的编码方式
在这里插入图片描述
1.String字符串对象
字符串对象支持INT、EMBSTR、ROW三种编码方式。
a.如果字符串的值是整数,同时可以使用long来进行表示,那么Redis将会使用INT编码方式。
b.如果字符串的值是字符,同时字符串的大小小于32个字节,那么Redis将会使用EMBSTR编码方式。
c.如果字符串的值是字符,同时字符串的大小大于32个字节,那么Redis将会使用ROW编码方式
编码转换:
如果字符串的值不再是整数或者用long无法进行表示,那么INT编码方式将会转换成ROW编码方式。
如果字符串的值其大小大于32个字节,那么EMBSTR编码方式将会转换成ROW编码方式。
*INT编码方式不能转换成EMBSTR编码方式。

2.List列表对象
列表对象支持ZIPLIST、LINKEDLIST两种编码方。
a.如果列表对象保存的元素的数量少于512个,同时每个元素的大小都小于64个字节,那么Redis将会使用ZIPLIST编码方式
b.如果列表对象保存的元素的数量多于512个,或者元素的大小大于64个字节,那么Redis将会使用LINKEDLIST编码方式
编码转换
如果列表对象保存的元素的数量多于512个,或者元素的大小大于64个字节,那么Redis将会使用LINKEDLIST编码方式。
可以通过list-max-ziplist-entries和list-max-ziplist-value参数,调整列表对象ZIPLIST编码方式中最多可以保存元素的个数以及每个元素的最大大小。

3.Hash哈希对象
哈希对象支持ZIPLIST和HASHTABLE两种编码方式。
a.如果哈希对象保存的键值对的数量少于512个,同时每个键值对中的键和值的大小都小于64个字节,那么Redis将会使用ZIPLIST编码方式:
b.如果哈希对象保存的键值对的数量多于512个,或者键值对中的键或值的大小大于64个字节,那么Redis将会使用HASHTABLE编码方式
编码转换
如果哈希对象保存的键值对的数量多于512个,或者键或值中的键和值的字符串的大小大于64个字节,那么Redis将会使用HASHTABLE编码方式。
可以通过hash-max-ziplist-entries和hash-max-ziplist-value参数,调整哈希对象ZIPLIST编码方式中最多可以保存元素的个数以及每个键值对中的键和值的字符串的最大大小。

4.Set集合对象
集合对象支持INTSET和HASHTABLE两种编码方式
a.如果集合对象保存的元素的数量少于512个,同时每个元素都是整数,那么Redis将会使用INTSET编码方式。
b.果集合对象保存的元素的数量多于512个,或者元素不是整数,那么Redis将会使用HASHTABLE编码方式
编码转换
如果集合对象保存的元素的数量多于512个,或者元素不是整数,那么Redis将会使用HASHTABLE编码方式。
可以通过set-max-intset-entries参数,调整集合对象INTSET编码方式中最多可以保存元素的个数。

5.zset有序集合对象
有序集合对象支持ZIPLIST和SKIPLIST两种编码方式
a.如果有序集合对象保存的元素的数量少于128个,同时每个元素的大小都小于64个字节,那么Redis将会使用ZIPLIST编码方式。
b.如果有序集合对象保存的元素的数量多于128个,或者元素的大小大于64个字节,那么Redis将会使用SKIPLIST编码方式
编码转换
如果有序集合对象保存的元素的数量多于128个,或者元素的大小大于64个字节,那么Redis将会使用SKIPLIST编码方式。
可以通过zset-max-ziplist-entries和zset-max-ziplist-value参数,调整有序集合对象ZIPLIST编码方式中最多可以保存元素的个数以及每个元素的最大大小。

Redis内存监控

可以使用info memory命令查看Redis内存的使用情况
used_memory:redis有效数据占用的内存大小(包括使用的虚拟内存)
uesd_memory_rss:redis有效数据占用的内存大小(不包括使用的虚拟内存)、redis进程所占用的内存大小、内存碎片(与TOP命令查看的内存一直)
mem_fragmentation_ratio(内存碎片率) = used_memory_rss / used_memory ,由于一般不会使用虚拟内存,同时redis进程所占用的内存相对使用的内存来说很少,因此这个比例可以认为是内存碎片率。
mem_allocator:redis内存分配器,可选jemalloc(默认)、libc、tcmalloc

*max_memory配置的是Redis有效数据最大可以使用的内存,由于存在内存碎片,因此Redis实际占用的内存大小最终一定会比max_memory要大。

关于内存碎片率
mem_fragmentation_ratio = used_memory_rss / used_memory ;
当内存碎片率 < 1时,表示redis正在使用虚拟内存,当内存碎片率严重 > 1,表示redis存在大量的内存碎片。
*内存碎片率在1~1.1之间是比较健康的状态。

产生内存碎片的原因
1.如果频繁修改value,且value的值相差很大,那么有可能导致编码转换或者已分配的内存大小不足,那么redis内存分配器需要重新为对象分配内存,然后释放掉对象之前所占用的内存,如果释放掉的内存无法被操作系统所回收,那么就形成了内存碎片。
2.redis的内存淘汰机制,根据内存淘汰策略删除一部分的Key,但释放的内存无法被操作系统所回收。
*根本原因是redis释放的内存无法被操作系统所回收。

解决内存碎片的方法
1.重启Redis服务,会自动读取RDB文件进行数据的恢复,重新为对象分配内存。
2.Redis4.0提供了清除内存碎片的功能
#自动清除
activedefrag yes

#手动执行命令清除
memory purge

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值