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

redis字符串匹配_Redis设计原理相关推荐

  1. redis字符串匹配_Redis的数据类型和抽象概念介绍

    Redis 不是一个 简单的 key-value 存储,实际上它是一个数据结构服务器,它支持不同类型的值.也就是说,在传统的key-value存储中,你将一个字符串的key关联到一个字符串的值上:而在 ...

  2. cell数组变为字符串_字符串匹配 ---- BM 算法原理

    关于字符串匹配有很多算法, BF, BK, KMP, 但这些都不是这篇文章的重点. 文章的重点是另外一种更高效的算法 Boyer-Moore 算法, 一般文本编辑器中的查找功能都是基于它实现的. 前置 ...

  3. Redis系列教程(五):Redis哨兵、复制、集群的设计原理,以及区别

    前一篇文章高并发架构系列:Redis为什么是单线程.及高并发快的3大原因详解谈了Redis高并发快的3个原因,本篇主要谈Redis的高可用,两篇合起来就可以把redis的高并发和高可用搞清楚了. 谈到 ...

  4. java 字符串匹配_多模字符串匹配算法原理及Java实现代码

    多模字符串匹配算法在这里指的是在一个字符串中寻找多个模式字符字串的问题.一般来说,给出一个长字符串和很多短模式字符串,如何最快最省的求出哪些模式字符串出现在长字符串中是我们所要思考的.该算法广泛应用于 ...

  5. 字符串匹配原理及实现(C++版)

    字符串匹配原理及实现(C++版) 1. 字符串匹配概念 2. BF 2.1 原理 2.2 代码实现 3. KMP 3.1 原理 3.2 代码实现 4. BM 4.1 坏字符 4.2 好后缀 4.3 代 ...

  6. 哨兵模式原理_Redis哨兵、复制、集群的设计原理,以及区别

    谈到Redis服务器的高可用,如何保证备份的机器是原始服务器的完整备份呢?这时候就需要哨兵和复制. 哨兵(Sentinel):可以管理多个Redis服务器,它提供了监控,提醒以及自动的故障转移的功能. ...

  7. Redis的高可用详解:Redis哨兵、复制、集群的设计原理,以及区别

    谈到Redis服务器的高可用,如何保证备份的机器是原始服务器的完整备份呢?这时候就需要哨兵和复制. 哨兵(Sentinel):可以管理多个Redis服务器,它提供了监控,提醒以及自动的故障转移的功能. ...

  8. 三张图秒懂Redis集群设计原理

    Redis集群设计包括2部分:哈希Slot和节点主从,本篇博文通过3张图来搞明白Redis的集群设计. 节点主从: 主从设计不算什么新鲜玩意,在数据库中我们也经常用主从来做读写分离,直接上图: 图上能 ...

  9. 算法设计:精确字符串匹配

    首先来学学精确字符串匹配: 那我们要怎么找呢?: 好麻烦,有没有别的?: 指纹就是一个标识符,但这个标识符怎么找呢? 就是把每个字母当成一位十进制数的数字,我开头记录下目标的字符串是什么数字,然后再一 ...

最新文章

  1. DeepMind出品:终于不瞎编了!AI学会了“谷歌一下”,回答问题正确率达90%。
  2. python 编程实例 1
  3. sql2005 Agent XPs 选项释义
  4. PCL:关于pcd数据显示乱码
  5. 《C++ Primer plus》学习笔记之”RTTI”
  6. 【论文解读】IPM2020 | 长短期兴趣建模的图神经网络新闻推荐系统
  7. gRPC .NET Core跨平台学习
  8. vim粘贴板和系统粘贴板的共享(linux)
  9. 音游android平板,音游专题 - 有时候听歌远远不够,试试这些音游吧 - Android 应用 - 【最美应用】...
  10. python如何将多张excel表内数据求和_Excel批量操作,把你的工作效率提升10倍以上(1)...
  11. 【HTML+CSS网页设计与布局 从入门到精通】第7章-class、ID选择器,CSS格式
  12. linux系统ssh服务无法启动,Linux上的SSH无法启动
  13. 用于创建二维数组的语法
  14. 如何在 Mac 上给文件、文件夹和磁盘重新命名?
  15. Ant Design Vue - 修改<Table>表格组件默认的暂无数据图标(自定义表格空数据状态图片)
  16. 全球最大的电子图书馆创世纪下载网址
  17. win,linux双系统开机引导修复
  18. 匹兹堡大学约翰斯敦计算机学院,英语翻译翻译Every human being,no matter what he is doing,gives off...
  19. PS 学习笔记 16 -渐变工具组
  20. 5款知乎高赞的超牛软件,你一定要知道

热门文章

  1. JavaScript(第七天)【对象和数组】
  2. 编程:请写一个类,在任何时候都可以向它查询“你已经创建了多少个对象?”...
  3. 用BULK INSERT命令导入数据详解
  4. golang学习笔记12 beego table name `xxx` repeat register, must be unique 错误问题
  5. EF中使用SQL语句或存储过程
  6. express 写接口
  7. php生成网页桌面快捷方式
  8. 算法是什么我记不住,But i do it my way. (二)
  9. css属性之box-shadow
  10. webapp构建工具库