Redis设计原理

1.简介

 

Redis中的每一个Key-Value在内存中都会被划分红DictEntry、RedisObject以及具体对象,其中DictEntry又分别包含指向Key和Value的指针(以RedisObject的形式)以及指向下一个DictEntry的指针。html

 

 

 

 

Key固定是字符串,所以使用字符串对象来进行表示,Value能够是字符串、列表、哈希、集合、有序集合对象中的任意一种。redis

Redis提供了五种对象,每种对象都须要使用RedisObject进行表示。数据库

 

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属性:存储对象被引用的次数。post

*ptr指针:指向对象的地址。性能

 

使用type命令能够查看对象的类型。优化

使用object encoding命令能够查看对象使用的编码方式。

使用object idletime命令能够查看对象的空转时间(即多久没有被访问,并不会刷新当前RedisObject的lru属性)

使用object refcount命令能够查看对象被引用的次数。

*这些命令都是经过Key找到对应的Value再从Value对应的RedisObject中进行获取。

 

 

2.字符串

 

Redis没有直接使用C语言的字符串,而是自定义了一种字符串类型,以对象的形式存在(C语言的字符串只是单纯的字面量,不可以进行修改)

Redis使用sdshdr结构来表示字符串对象(SDS)

struct sdshdr { int len; int free; char buf[]; };

len属性:字符串的长度。

free属性:未使用的字节数量。

buf数组:字符串的底层实现用于存储字符。

 

*buf数组中会有\0空字符,该空字符不会记录在len属性中。

 

SDS相比C语言的字符串

C语言中存储字符串的字节数组其长度老是N+1(最后一个是结束符),所以一旦对字符串进行追加则须要从新分配内存。

为了不C字符串的这种缺陷,SDS经过未使用的空间解除了字符串长度和底层数组长度之间的关系,在SDS中buf数组的长度不必定就是字符串长度+1,数组里面还能够包含未使用的字节。

经过未使用的空间,SDS实现了空间预分配惰性空间释放两种策略,从而减小因为字符串的修改致使内存重分配的次数。

空间预分配:用于优化SDS保存的字符串的增加操做,当须要对SDS保存的字符串进行增加操做时,程序除了会为SDS分配所必须的空间之外,还会为SDS分配额外的未使用空间。

惰性空间释放:用于优化SDS保存的字符串的缩短操做,当须要对SDS保存的字符串进行缩短操做时,程序并不会当即使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些多出来的字节的数量记录出来,等待未来使用。

 

 

3.字典

 

Redis的字典使用散列表做为底层实现,同时字典也是Redis数据库和HashTable编码方式的底层实现。

 

Redis使用dictht结构来表示散列表

typedef struct dictht { dictEntry **table; unsigned long size; unsigned long sizemask; unsigned long used; }dictht;

table属性:散列表。

size属性:散列表的大小。

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

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

*Redis的散列表使用链地址法的方式解决散列冲突,最终就是指针数组的形式,数组中的每一个元素都是一个指向DictEntry的指针。


Redis使用dictEntry结构来表示散列表中的节点

复制代码
typedef struct dictEntry { void *key; union{ void *val; uint_tu64; int64_ts64; }v struct dictEntry next*; }dictEntry; 
复制代码

key属性:指向Key的指针(即RedisObject)

value属性:能够是一个指向Value的指针(即RedisObject)、uint64_t整数、int64_t整数

next属性:指向下一个DictEntry的指针。


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); //复制Key的函数
    void *(*keyDup)(void *privatedata, const void *key); //复制Value的函数
    void *(*valDup)(void *privatedata, const void *obj); //对比Key的函数
    int (*keyCompare)(void *privatdata, const void *key1 , const void *key2); //销毁Key的函数
    void (*keyDestructor)(void *privatedata, void *key); //销毁Value的函数
    void (*valDestructor)(void *privatedata, void *obj); }dictType;
复制代码

 

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

 

在字典中进行查找

以客户端传递的Key做为关键字K,经过dict中的dictType的H(K)散列函数计算散列值,使用dictht[0]的sizemask属性和散列值计算索引,遍历索引对应的链表,判断是否存在Key相同的DictEntry,若存在则返回该DictEntry,不然返回NULL。

 

在字典中进行添加和更新操做

以客户端传递的Key做为关键字K,经过dict中的dictType的H(K)散列函数计算散列值,使用dictht[0]的sizemask属性和散列值计算索引,遍历索引对应的链表,判断是否存在Key相同的DictEntry,若不存在Key相同的DictEntry,则建立表明Key的SDS对象和RedisObject以及表明Value的对象和RedisObject,而后建立一个DictEntry并分别指向Key和Value对应的RedisObject,最终将该DictEntry追加到链表的最后一个节点中,若存在Key相同的DictEntry,则判断当前的命令是否知足Value对应的类型,若知足则进行更新,不然报错。

*建立和更新操做是相对的,当不存在则建立不然进行更新。

 

在字典中进行删除操做

以客户端传递的Key做为关键字K,经过dict中的dictType的H(K)散列函数计算散列值,使用dictht[0]的sizemask属性和散列值计算索引,遍历索引对应的链表,找到Key相同的DictEntry进行删除。

 

3.2 散列表的扩容和缩容

因为散列表的负载因子须要维持在一个合理的范围内,所以当散列表中的元素过多时会进行扩容,过少时会进行缩容。

一旦散列表的长度发生改变,那么就要进行rehash,即对原先散列表中的元素在新的散列表中从新进行hash。

Redis中的rehash是渐进式的,并非一次性完成,由于要考虑性能问题,若是散列表中包含上百万个节点,那么庞大的计算量可能会致使Redis在一段时间内没法对外提供服务。

在rehash进行期间,每次对字典执行查找、添加、更新、删除操做时,除了会执行指定的操做之外,还会顺带将ht[0]散列表在rehashidx索引上的全部节点rehash到ht[1]上,而后将rehashidx属性的值加1。

 

渐进式Rehash的步骤

1.为字典的ht[1]散列表分配空间。

*若执行的是扩容操做,那么ht[1]的长度为第一个大于等于ht[0].used*2的2ⁿ。 

*若执行的是缩容操做,那么ht[1]的长度为第一个大于等于ht[0].used的2ⁿ。

2.rehashidx属性设置为0,表示开始进行rehash。

3.在rehash进行期间,每次对字典执行查找、添加、更新、删除操做时,除了会执行指定的操做之外,还会顺带将ht[0]散列表在rehashidx索引上的全部节点rehash到ht[1]上,而后将rehashidx属性的值加1。

4.随着对字典不断的操做,最终在某个时间点上,ht[0]散列表中的全部dictEntry都会被rehash到ht[1]上,当rehash结束以后将rehashidx属性的值设为-1,表示rehash操做已完成。

*在进行渐进式rehash的过程当中,字典会同时使用ht[0]和ht[1]两个散列表,所以字典的查找、更新、删除操做会在两个散列表中进行,若是在ht[0]计算获得的索引指向NULL则从ht[1]中进行匹配。

 

 

4.Redis提供的编码方式

 

Redis提供了八种编码方式,每种编码方式都有其特定的数据存储结构。

 

4.1 INT编码方式

INT编码方式会将RedisObject中的*ptr指针直接改写成long prt,prt属性直接存储整数值。

 

4.2 EMBSTR编码方式

 

4.3 ROW编码方式

 

*EMBSTR和ROW编码方式在内存中都会建立一个RedisObject和SDS,区别在于EMBSTR编码方式中RedisObject和SDS共同使用同一块内存单元,Redis内存分配器只须要分配一次内存,而ROW编码方式中须要分别为RedisObject和SDS分配内存单元。

 

4.4 ZIPLIST编码方式

压缩列表是Redis为了节约内存而开发的,它是一块顺序表(顺序存储结构,内存空间连续),一个压缩列表中能够包含多个entry节点,每一个entry节点能够保存一个字节数组或者一个整数值。

zlbytes:记录了压缩列表的大小,占4个字节。

zltail:记录了压缩列表表尾节点距离起始位置的大小,占4个字节。

zllen:记录了压缩列表中节点的个数,占2个字节。

entry:压缩列表中的节点,大小由节点中保存的内容决定。

zlend:压缩列表的结束标志,占1个字节。

 

若是存在一个指针P指向压缩列表的起始位置,就能够根据P+zltail获得最后一个节点的地址。

 

4.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属性:存储链表中节点的个数。

 

4.6 INTSET编码方式

 

 

 

Redis使用intset结构来表示整数集合。

typedef struct inset { uint32_t encoding; uint32_t length; int8_t contents[]; }intset;

encoding属性:contents数组的类型,支持INTESET_ENC_INT1六、INTESET_ENC_INT3二、INTESET_ENC_INT64。

length属性:存储整数集合中元素的个数。

contents数组:整数集合的底层实现,集合中的每一个元素在数组中都会按照值从小到大进行排序同时保证元素不会重复。

 

Contents升级

当往数组中添加一个比当前数组类型还要大的元素时,将要进行升级。

 

1.根据新元素的类型对数组进行扩容( (length + 1) * 新类型大小)

 

2.将数组中现有的元素都转换成与新元素相同的类型,并将转换后的元素移动到正确的位置上。

 

3.将新元素添加到数组中。

 

4.修改intset中的encoding属性为新的类型。

 

Contents降级

contents数组不支持降级,一旦为contents数组进行了升级那么就要一直保持升级后的状态。

 

4.7 HT编码方式

 

4.8 SKIPLIST编码方式

经过在每一个节点中维持多个指向其余节点的指针,从而达到快速访问节点的目的。

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属性:指向元素的指针(redisObject->sds)

每次建立一个新的跳跃表节点时,会随机生成一个介于1到32之间的值做为level数组的大小。

 

Redis使用zskiplist结构来持有zskiplistNode

typedef struct zskiplist { struct zskiplistNode *header,*tail; unsigned long length; int level; }zskiplist;

header属性:指向表头节点的指针。

tail属性:指向表尾节点的指针。

length属性:存储跳跃表中节点的个数,不包括表头节点。

level属性:跳跃表中节点level的最大值,不包括表头节点。

*跳跃表中存在表头节点,表头节点一共有32个level,即数组的大小为32。

 

遍历zskiplist的流程

1.经过zskiplist访问跳跃表中的头节点。

2.从下一个节点最高的level开始往下遍历,若下一个节点的最高level超过当前节点的最高level,则从当前节点最高的level开始往下遍历。

3.当不存在下一个节点时,遍历结束。

 

 

5.Redis对象

 

Redis各个对象支持的编码方式

 

5.1 字符串对象

字符串对象支持INT、EMBSTR、ROW三种编码方式

 

INT编码方式

若是字符串的值是整数,而且可使用long来进行表示,那么Redis将会使用INT编码方式。


INT编码方式会将RedisObject中的*ptr指针直接改写成long prt,prt属性直接存储整数值。


EMBSTR编码方式

若是字符串的值是字符,而且其长度小于32个字节,那么Redis将会使用EMBSTR编码方式。

ROW编码方式

若是字符串的值是字符,而且其长度大于32个字节,那么Redis将会使用ROW编码方式。

 

*EMBSTR和ROW编码方式在内存中都会建立一个RedisObject和SDS,区别在于EMBSTR编码方式中RedisObject和SDS共同使用同一块内存单元,Redis内存分配器只须要分配一次内存,而ROW编码方式中须要分别为RedisObject和SDS分配内存单元。


编码转换

若是字符串的值再也不是整数或者用long没法进行表示,那么INT编码方式将会转换成ROW编码方式。

若是字符串的值其长度大于32个字节,那么EMBSTR编码方式将会转换成ROW编码方式。

*INT编码方式和EMBSTR编码方式在知足条件的状况下,将会转换成ROW编码方式。

*INT编码方式不能转换成embstr编码方式。

 

字符串共享对象

Redis在启动时会初始化值为0~9999的SDS做为共享对象,当set一个Key其Value是在0~9999范围时,会直接使用该共享对象,DictEntry中的Value指针直接指向该共享SDS对应的RedisObject。

在集群模式中,Redis的每一个节点启动时都会初始化值为0~9999的SDS做为共享对象。

在RedisV4.0以上,使用Object refcount命令再也不返回共享对象实际被引用的次数,而是直接返回Integer.MAX_VALUE。

 

 

5.2 列表对象

列表对象支持ZIPLIST、LINKEDLIST两种编码方式

 

ZIPLIST编码方式

若是列表对象保存的全部元素的长度都小于64个字节同时元素的数量小于512个,那么Redis将会使用ZIPLIST编码方式。

 

LINKEDLIST编码方式

若是列表对象保存的元素的长度大于64个字节或元素的数量大于512个,那么Redis将会使用LINKEDLIST编码方式。

 

编码转换

若是列表对象保存的元素的长度大于64个字节或元素的数量大于512个,那么Redis将会使用LINKEDLIST编码方式。

能够经过list-max-ziplist-value和list-max-ziplist-entries参数调整列表对象ZIPLIST编码方式所容许保存的元素的最大值以及最多能够保存元素的数量。

 

 

5.3 哈希对象

哈希对象支持ZIPLIST和HT两种编码方式。

 

ZIPLIST编码方式

若是哈希对象保存的全部键值对的键和值的字符串长度都小于64个字节同时键值对的数量小于512个,那么Redis将会使用ZIPLIST编码方式。

 

 

HT编码方式

若是哈希对象保存的键值对的键或值的字符串长度大于64个字节或键值对的数量大于512个,那么Redis将会使用HASHTABLE编码方式。

 

编码转换

若是哈希对象保存的键值对的键或值的字符串长度大于64个字节或键值对的数量大于512个,那么Redis将会使用HASHTABLE编码方式。

能够经过hash-max-ziplist-value和hash-max-ziplist-entries参数调整哈希对象ZIPLIST编码方式所容许保存的元素的最大值以及最多能够保存元素的数量。

 

 

5.4 集合对象

集合对象支持INTSET和HT两种编码方式

 

INTSET编码方式

若是集合对象保存的全部元素都是整数同时元素的数量不超过512个,那么Redis将会使用INTSET编码方式。

 

HT编码方式

若是集合对象保存的元素并非整数或元素的数量超过512个,那么Redis将会使用HASHTABLE编码方式。

 

编码转换

若是集合对象保存的元素并非整数或元素的数量超过512个,那么Redis将会使用HASHTABLE编码方式。

能够经过set-max-intset-entries参数调整集合对象INTSET编码方式最多能够保存元素的数量。

 

 

5.5 有序集合对象

有序集合对象支持ZIPLIST和SKIPLIST两种编码方式。

 

ZIPLIST编码方式

若是有序集合对象保存的全部元素的字符串长度都小于64个字节同时元素的数量不超过128个,那么Redis将会使用ZIPLIST编码方式。

 

SKIPLIST编码方式

若是有序集合对象保存的元素的字符串长度大于64个字节或元素的数量超过128个,那么Redis将会使用SKIPLIST编码方式。

 

编码转换

若是有序集合对象保存的元素的字符串长度大于64个字节或元素的数量超过128个,那么Redis将会使用SKIPLIST编码方式。

能够经过zset-max-ziplist-value和zset-max-ziplist-entries参数调整有序集合对象ZIPLIST编码方式所容许保存的元素的最大值以及最多能够保存元素的数量。

 

 

6.Redis内存分配器

 

Redis提供了jemalloc、libc、tcmalloc内存分配器,默认使用jemalloc,须要在编译时指定。

 

Jemalloc内存分配器

jemalloc内存分配器将内存划分为小、大、巨大三个范围,每一个范围又包含多个大小不一样的内存单元。

DictEntry、RedisObject以及对象在初始化时,Redis内存分配器都分配一个合适的内存大小。

若是频繁修改Value,且Value的值相差很大,那么Redis内存分配器须要从新为对象分配内存,而后释放掉对象以前所占用的内存(编码转换或者数组越界)

 

 

7.Redis内存监控

 

可使用info memory命令查看Redis内存的使用状况

used_memory:redis有效数据占用的内存大小(包括使用的虚拟内存)

uesd_memory_rss:redis有效数据占用的内存大小(不包括使用的虚拟内存)、redis进程所占用的内存大小、内存碎片(与TOP命令查看的内存一直)

mem_fragmentation_ratio(内存碎片率) = used_memory_rss / used_memory

mem_allocator:redis内存分配器,可选jemalloc(默认)、libc、tcmalloc

*max_memory配置的是Redis有效数据最大可以使用的内存大小,不包括内存碎片,所以Redis实际占用的内存大小最终必定会比max_memory要大。

 

内存碎片率

1.当内存碎片率 < 1时,表示redis正在使用虚拟内存。

2.当内存碎片率严重 > 1,表示redis存在大量的内存碎片。

*内存碎片率在1~1.1之间是比较健康的状态。

有可能产生内存碎片的操做:频繁更新Value且Value的值相差很大(从新为对象分配内存,释放以前的内存)、Redis的内存淘汰机制。

产生内存碎片的根本缘由:Redis释放的内存没法被操做系统所回收。

 

解决内存碎片的方法

1.重启Redis服务,会从新读取RDB文件进行数据的恢复,从新为对象分配内存。

2.Redis4.0提供了清除内存碎片的功能

#运行期自动清除
activedefrag yes

#手动执行命令清除
memory purge

 

 

8.Redis监视器

 

客户端向服务器发送命令请求时,服务器除了会执行相应的命令之外,还会将关于这条命令请求的信息转发给全部的监视器。

经过执行monitor命令,客户端能够将本身变成一个监视器,实时接收服务器当前正在执行的命令请求的相关信息。

 

« 上一篇: Nginx反向代理
posted @ 2019-09-05 11:13  辣鸡小篮子 阅读( 99) 评论( 0) 编辑 收藏
相关文章
相关标签/搜索