Redis设计原理

1.简介

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

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

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

*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_INT16、INTESET_ENC_INT32、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命令,客户端可以将自己变成一个监视器,实时接收服务器当前正在执行的命令请求的相关信息。

原文地址:https://www.cnblogs.com/ylaoda/p/11465057.html

时间: 2024-10-12 06:59:37

Redis设计原理的相关文章

mongodb和redis设计原理简析

转自:http://blog.csdn.net/yangbutao/article/details/8309539 redis: 1.NIO通信 因都在内存操作,所以逻辑的操作非常快,减少了CPU的切换开销,所以为单线程的模式(逻辑处理线程和主线程是一个). reactor模式,实现自己的多路复用NIO机制(epoll,select,kqueue等) 单线程处理多任务 2.数据结构 hash+bucket结构,当链表的长度过长时,会采取迁移的措施(扩展原来两倍的hash表,把数据迁移过去,exp

全面剖析Redis Cluster原理和应用

全面剖析Redis Cluster原理和应用 1.Redis Cluster总览 1.1 设计原则和初衷 在官方文档Cluster Spec中,作者详细介绍了Redis集群为什么要设计成现在的样子.最核心的目标有三个: 性能:这是Redis赖以生存的看家本领,增加集群功能后当然不能对性能产生太大影响,所以Redis采取了P2P而非Proxy方式.异步复制.客户端重定向等设计,而牺牲了部分的一致性.使用性. 水平扩展:集群的最重要能力当然是扩展,文档中称可以线性扩展到1000结点. 可用性:在Cl

Redis设计与实现(一~五整合版)【搬运】

Redis设计与实现(一~五整合版) by @飘过的小牛 一 前言 项目中用到了redis,但用到的都是最最基本的功能,比如简单的slave机制,数据结构只使用了字符串.但是一直听说redis是一个很牛的开源项目,很多公司都在用.于是我就比较奇怪,这玩意不就和 memcache 差不多吗?仅仅是因为memcache是内存级别的,没有持久化功能.而redis支持持久化?难道这就是它的必杀技? 带着这个疑问,我在网上搜了一圈.发现有个叫做huangz的程序员针对redis写了一本书叫做<redis设

《Redis设计与实现》

<Redis设计与实现> 基本信息 作者: 黄健宏 丛书名: 数据库技术丛书 出版社:机械工业出版社 ISBN:9787111464747 上架时间:2014-6-3 出版日期:2014 年6月 开本:16开 页码:1 版次:1-1 所属分类:计算机 > 数据库 > 数据库理论 > 综合 更多关于>>> <Redis设计与实现>   内容简介 书籍 计算机书籍 <redis设计与实现>全面而完整地讲解了redis的内部机制与实现方式,

《Redis设计与实现剖析- 前言》

现如今Redis已经不折不扣的成为缓存技术中的主流中间件,基本上大型的系统都会选择Redis缓存来提升系统性能. 由于在目前开发项目中也有使用Redis,在使用以及了解Redis的过程中被Redis优秀的设计与实现所吸引,Redis本身是基于C语言实现的高级应用,Redis内部也大量使用了经典数据结构(数组,链表,Hash表,队列,堆,跳跃表),刚好最近在巩固加深数据结构与算法这方面的基本功,所以就萌出了通过剖析Redis内部实现的方式,来复习巩固C语言的知识及其高级应用和经典数据结构的原理及其

Redis设计与实现 pdf扫描版【65M】高清下载

<Redis设计与实现>全面而完整地讲解了Redis的内部机制与实现方式,对Redis的大多数单机功能以及所有多机功能的实现原理进行了介绍,展示了这些功能的核心数据结构以及关键的算法思想,图示丰富,描述清晰,并给出大量参考信息.通过阅读本书,读者可以快速.有效地了解Redis的内部构造以及运作机制,更好.更高效地使用Redis. <Redis设计与实现>主要分为四大部分.第一部分“数据结构与对象”介绍了Redis中的各种对象及其数据结构,并说明这些数据结构如何影响对象的功能和性能.

kafka入门:简介、使用场景、设计原理、主要配置及集群搭建(转)

问题导读: 1.zookeeper在kafka的作用是什么? 2.kafka中几乎不允许对消息进行"随机读写"的原因是什么? 3.kafka集群consumer和producer状态信息是如何保存的? 4.partitions设计的目的的根本原因是什么? 一.入门 1.简介 Kafka is a distributed,partitioned,replicated commit logservice.它提供了类似于JMS的特性,但是在设计实现上完全不同,此外它并不是JMS规范的实现.k

Atitit.ioc&#160;动态配置文件guice&#160;设计原理

Atitit.ioc 动态配置文件guice 设计原理 1. Bat启动时注入配置文件1 2. ioc调用1 3. Ioc 分发器 配合 apche  MethodUtils.invokeStaticMethod2 1. Bat启动时注入配置文件 SET JAVA_HOME=C:\Program Files\Java\jdk1.8.0_71 set  RESIN-HOME=c:\resin-4.0.22 set classpath=%classpath%;%RESIN-HOME%\lib\jas

BigPipe设计原理

高性能页面加载技术--BigPipe设计原理及Java简单实现 1.技术背景 动态web网站的历史可以追溯到万维网初期,相比于静态网站,动态网站提供了强大的可交互功能.经过几十年的发展,动态网站在互动性和页面显示效果上有了很大的提升,但是对于网站动态网站的整体页面加载架构没有做太大的改变.对于用户而言,页面的加载速度极大的影响着用户体验感.与静态网站不同,除了页面的传输加载时间外,动态网站还需考虑服务端数据的处理时间.像facebook这样大型的用户社交网站,必须考虑用户访问速度问题, 传统we