Redis3 对象类型与类型编码

前面已经说过,Redis支持5种对象类型,而每种结构都有至少两种编码;

这样做的好处在于: 一方面接口与实现分离,当需要增加或改变内部编码时,用户使用不受影响;
另一方面可以根据不同的应用场景切换内部编码,提高效率。

Redis各种对象类型支持的内部编码如下图所示(只列出重点的):

类型编码OBJECT ENCODING 命令输出对象
REDIS STRINGREDIS_ENCODING_INT“int”使用整数值实现的字符串对象
REDIS STRINGREDIS_ENCODING_ EMBSTR“embstr”使用embstr编码的简单动态字符串实现的字符串对象
REDIS STRINGREDIS _ENCODING_RAW“raw”使用简单动志字符串实现的宇符串对象
REDIS LISTREDIS_ENCODING_ZIPLIST“ziplist”使用压缩列表实现的列表对象
REDIS LISTREDIS_ENCODING_LINKEDLIST“linkedlist”使用双端链表实现的列表对象
REDIS HASHREDIS_ENCODING_LINKEDLIST“ziplist”使用压缩列表实现的哈希对象
REDIS HASHREDIS_ENCODING_HT"hashtabl’’使用字典实现的哈希对象
REDIS SETREDIS_ENCODING_INTSET“intset”使用整数集合实现的集合对象
REDIS SETREDIS_ENCODING_HT“hashtable”使用字典实现的集合对象
REDIS ZSETREDIS_ENCODING_ZIPLIST“ziplist”使用压缩列表实现的有序集合对象
REDIS ZSETREDIS_ENCODING_ SKIPLIST“skiplist”使用跳跃表和字典实现的有序集合对象

一、字符串

(1)概况

字符串是最基础的类型,因为所有的键都是字符串类型,且字符串之外的其他几种复杂类型的元素也是字符串。
字符串长度不能超过512MB。

(2)内部编码

字符串类型的内部编码有3种,它们的应用场景如下:

  • int:8个字节的长整型。字符串值是整型时,这个值使用long整型表示。
  • embstr:<=44字节的字符串。embstr与raw都使用redisObject和sds保存数据,区别在于,embstr的使用只分配一次内存空间(因此redisObject和sds是连续的),而raw需要分配两次内存空间(分别为redisObject和sds分配空间)。因此与raw相比,embstr的好处在于创建时少分配一次空间,删除时少释放一次空间,以及对象的所有数据连在一起,寻找方便。而embstr的坏处也很明显,如果字符串的长度增加需要重新分配内存时,整个redisObject和sds都需要重新分配空间,因此redis中的embstr实现为只读。
  • raw:大于44个字节的字符串

embstr和raw进行区分的长度,是44;是因为redisObject的长度是16字节,sds的长度是9+字符串长度;因此当字符串长度是39时,embstr的长度正好是16+9+39=64,jemalloc正好可以分配64字节的内存单元。

(3)编码转换

当int数据不再是整数,或大小超过了long的范围时,自动转化为raw。
而对于embstr,由于其实现是只读的,因此在对embstr对象进行修改时,都会先转化为raw再进行修改,因此,只要是修改embstr对象,修改后的对象一定是raw的,无论是否达到了44个字节。

二、列表

linkedlist和arraylist的区别

(1)概况

列表(list)用来存储多个有序的字符串,每个字符串称为元素;

一个列表可以存储2^32-1个元素。 Redis中的列表支持两端插入和弹出,并可以获得指定位置(或范围)的元素,可以充当数组、队 列、栈等。

(2)内部编码

列表的内部编码可以是压缩列表(ziplist)或双端链表(linkedlist)。

  • 双端类表

双端链表:由一个list结构和多个listNode结构组成;
典型结构如下图所示:
在这里插入图片描述

通过图中可以看出,双端链表同时保存了表头指针和表尾指针,并且每个节点都有指向前和指向后的指针;链表中保存了列表的长度;dup、free和match为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。而链表中每个节点指向的是type为字符串的redisObject。

  • 压缩列表
    压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块(而不是像双端链表一样每个节点是指针)组成的顺序型数据结构。

与双端链表相比,压缩列表可以节省内存空间,但是进行修改或增删操作时,复杂度较高;因此当节点数量较少时,可以使用压缩列表;但是节点数量多时,还是使用双端链表划算。

压缩列表不仅用于实现列表,也用于实现哈希、有序列表;使用非常广泛。

(3)编码转换

只有同时满足下面两个条件时,才会使用压缩列表:

  • 列表中元素数量小于512个;
  • 列表中所有字符串对象都不足64字节。

如果有一个条件不满足,则使用双端列表;且编码只可能由压缩列表转化为双端链表,反方向则不可能。

其中,单个字符串不能超过64字节,是为了便于统一分配每个节点的长度;这里的64字节是指字符串的长度,不包括SDS结构,因为压缩列表使用连续、定长内存块存储字符串,不需要SDS结构指明长度。后面提到压缩列表,也会强调长度不超过64字节,原理与这里类似。

三、哈希

(1)概况

哈希(作为一种数据结构),不仅是redis对外提供的5种对象类型的一种(与字符串、列表、集合、有序结合并列),也是Redis作为Key-Value数据库所使用的数据结构。为了说明的方便,后面当使用“内层的哈希”时,代表的是redis对外提供的5种对象类型的一种;使用“外层的哈希”代指Redis作为KeyValue数据库所使用的数据结构。

(2)内部编码

内层的哈希使用的内部编码可以是压缩列表(ziplist)和哈希表(hashtable)两种;Redis的外层的哈希则只使用了hashtable。

压缩列表前面已介绍。与哈希表相比,压缩列表用于元素个数少、元素长度小的场景;其优势在于集中存储,节省空间;
同时,虽然对于元素的操作复杂度也由O(1)变为了O(n),但由于哈希中 元素数量较少,因此操作的时间并没有明显劣势。

hashtable:一个hashtable由1个dict结构、2个dictht结构、1个dictEntry指针数组(称为bucket)和多个dictEntry结构组成。
正常情况下(即hashtable没有进行rehash时)各部分关系如下图所示:
在这里插入图片描述

下面从底层向上依次介绍各个部分:

dictEntry

dictEntry结构用于保存键值对,结构定义如下:

typedef struct dictEntry{ 
	void *key; 
	union{
		void *val; 
		uint64_tu64; 
		int64_ts64; 
	}v; 
	struct dictEntry *next; 
}dictEntry;

其中,各个属性的功能如下:

  • key:键值对中的键;
  • val:键值对中的值,使用union(即共用体)实现,存储的内容既可能是一个指向值的指针,也可能是64位整型,或无符号64位整型;
  • next:指向下一个dictEntry,用于解决哈希冲突问题

在64位系统中,一个dictEntry对象占24字节(key/val/next各占8字节)。

bucket

bucket是一个数组,数组的每个元素都是指向dictEntry结构的指针。redis中bucket数组的大小计算规则如下:大于dictEntry的、最小的2^n;

例如,如果有1000个dictEntry,那么bucket大小为1024;如果有1500个dictEntry,则bucket大小为2048。

dictht

dictht结构如下:

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

其中,各个属性的功能说明如下:

  • table属性是一个指针,指向bucket;
  • size属性记录了哈希表的大小,即bucket的大小;
  • used记录了已使用的dictEntry的数量;
  • sizemask属性的值总是为size-1,这个属性和哈希值一起决定一个键在table中存储的位置。
dict

一般来说,通过使用dictht和dictEntry结构,便可以实现普通哈希表的功能;但是Redis的实现中,在dictht结构的上层,还有一个dict结构。下面说明dict结构的定义及作用。
dict结构如下:

typedef struct dict{ 
	dictType *type; 
	void *privdata; 
	dictht ht[2]; 
	int trehashidx; 
} dict;
  • 其中,type属性和privdata属性是为了适应不同类型的键值对,用于创建多态字典。
  • ht属性和trehashidx属性则用于rehash,即当哈希表需要扩展或收缩时使用。ht是一个包含两个项的数组,每项都指向一个dictht结构,这也是Redis的哈希会有1个dict、2个dictht结构的原因。通常情况下,所有的数据都是存在放dict的ht[0]中,ht[1]只在rehash的时候使用。dict进行rehash操作的时候,将ht[0]中的所有数据rehash到ht[1]中。然后将ht[1]赋值给ht[0],并清空ht[1]。

因此,Redis中的哈希之所以在dictht和dictEntry结构之外还有一个dict结构,一方面是为了适应不同类型的键值对,另一方面是为了rehash。

(3)编码转换

如前所述,Redis中内层的哈希既可能使用哈希表,也可能使用压缩列表。
只有同时满足下面两个条件时,才会使用压缩列表:

  • 哈希中元素数量小于512个;
  • 哈希中所有键值对的键和值字符串长度都小于64字节。

如果有一个条件不满足,则使用哈希表;且编码只可能由压缩列表转化为哈希表,反方向则不可能。

四、集合

(1)概况

集合(set)与列表类似,都是用来保存多个字符串,但集合与列表有两点不同:集合中的元素是无序的,因此不能通过索引来操作元素;集合中的元素不能有重复。

一个集合中最多可以存储2^32-1个元素;除了支持常规的增删改查,Redis还支持多个集合取交 集、并集、差集。

(2)内部编码

集合的内部编码可以是整数集合(intset)或哈希表(hashtable)。
哈希表前面已经讲过,这里略过不提;需要注意的是,集合在使用哈希表时,值全部被置为null。
整数集合的结构定义如下:

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

其中,encoding代表contents中存储内容的类型,虽然contents(存储集合中的元素)是int8_t类型,但实际上其存储的值是int16_t、int32_t或int64_t,具体的类型便是由encoding决定的;length表示元素个数。

整数集合适用于集合所有元素都是整数且集合元素数量较小的时候,与哈希表相比,整数集合的优势在于集中存储,节省空间;同时,虽然对于元素的操作复杂度也由O(1)变为了O(n),但由于集合数量较少,因此操作的时间并没有明显劣势。

(3)编码转换

只有同时满足下面两个条件时,集合才会使用整数集合:

  • 集合中元素数量小于512个;
  • 集合中所有元素都是整数值。

如果有一个条件不满足,则使用哈希表;且编码只可能由整数集合转化为哈希表,反方向则不可能。

5、有序集合

(1)概况

有序集合与集合一样,元素都不能重复;但与集合不同的是,有序集合中的元素是有顺序的。与列表使用索引下标作为排序依据不同,有序集合为每个元素设置一个分(score)作为排序依据。

(2)内部编码

  • 有序集合的内部编码可以是压缩列表(ziplist)或跳跃表(skiplist)。ziplist在列表和哈希中都有使用,前面已经讲过,这里略过不提。
  • 跳跃表是一种有序数据结构,通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。除了跳跃表,实现有序数据结构的另一种典型实现是平衡树;大多数情况下,跳跃表的效率可以和平衡树媲美,且跳跃表实现比平衡树简单很多,因此redis中选用跳跃表代替平衡树。跳跃表支持平均O(logN)、最坏O(N)的复杂点进行节点查找,并支持顺序操作。Redis的跳跃表实现由zskiplist和zskiplistNode两个结构组成:前者用于保存跳跃表信息(如头结点、尾节点、长度等),后者用于表示跳跃表节点。
  • 跳跃表结构如下图
    在这里插入图片描述
    查询
  • 查找一个节点时,我们只需从高层到低层,一个个链表查找,每次找到该层链表中小于等于目标节点的最大节点,直到找到为止。由于高层的链表迭代时会“跳过”低层的部分节点,所以跳跃表会比正常的链表查找少查部分节点,这也是skiplist名字的由来。
  • 例如:
    查找46 : 55—21—55–37–55–46

插入

  • 跳跃表的数据插入,从楼层的角度来说,是从1层开始插入,至于是否有其他层,需要结合概率算法(抛硬币)。
  • L1 层
    概率算法
    在此还是以上图为例:跳跃表的初试状态如下图,表中没有一个元素:
    在这里插入图片描述
    如果我们要插入元素2,首先是在底部插入元素2,如下图:
    在这里插入图片描述
    然后我们抛硬币,结果是正面,那么我们要将2插入到L2层,如下图
    在这里插入图片描述
    继续抛硬币,结果是反面,那么元素2的插入操作就停止了,插入后的表结构就是上图所示。接下来,我们插入元素33,跟元素2的插入一样,现在L1层插入33,如下图:
    在这里插入图片描述
    然后抛硬币,结果是反面,那么元素33的插入操作就结束了,插入后的表结构就是上图所示。接下来,
    我们插入元素55,首先在L1插入55,插入后如下图:
    在这里插入图片描述
    然后抛硬币,结果是正面,那么L2层需要插入55,如下图:
    在这里插入图片描述
  • 以此类推,我们插入剩余的元素。当然因为规模小,结果很可能不是一个理想的跳跃表。但是如果元素个数n的规模很大,学过概率论的同学都知道,最终的表结构肯定非常接近于理想跳跃表(隔一个一跳)。

(3)编码转换

只有同时满足下面两个条件时,才会使用压缩列表:

  • 有序集合中元素数量小于128个;有序集合中所有
  • 成员长度都不足64字节。

如果有一个条件不满足,则使用跳跃表;且编码只可能由压缩列表转化为跳跃表,反方向则不可能。

五、内存优化

1、优化内存占用

了解redis的内存模型,对优化redis内存占用有很大帮助。下面介绍几种优化场景。

(1)利用jemalloc特性进行优化
  • 上一小节所讲述的90000个键值便是一个例子。由于jemalloc分配内存时数值是不连续的,因此key/value字符串变化一个字节,可能会引起占用内存很大的变动;在设计时可以利用这一点。
  • 例如,如果key的长度如果是8个字节,则SDS为17字节,jemalloc分配32字节;此时将key长度缩减为7个字节,则SDS为16字节,jemalloc分配16字节;则每个key所占用的空间都可以缩小一半。
(2)使用整型/长整型

如果是整型/长整型,Redis会使用int类型(8字节)存储来代替字符串,可以节省更多空间。因此在可以使用长整型/整型代替字符串的场景下,尽量使用长整型/整型。

(3)共享对象
  • 利用共享对象,可以减少对象的创建(同时减少了redisObject的创建),节省内存空间。目前redis中的共享对象只包括10000个整数(0-9999);可以通过调整REDIS_SHARED_INTEGERS参数提高共享对象的个数;例如将REDIS_SHARED_INTEGERS调整到20000,则0-19999之间的对象都可以共享。
  • 考虑这样一种场景:论坛网站在redis中存储了每个帖子的浏览数,而这些浏览数绝大多数分布在0-20000之间,这时候通过适当增大REDIS_SHARED_INTEGERS参数,便可以利用共享对象节省内存空间。
(4)避免过度设计
  • 然而需要注意的是,不论是哪种优化场景,都要考虑内存空间与设计复杂度的权衡;而设计复杂度会影响到代码的复杂度、可维护性。
  • 如果数据量较小,那么为了节省内存而使得代码的开发、维护变得更加困难并不划算;还是以前面讲到的90000个键值对为例,实际上节省的内存空间只有几MB。但是如果数据量有几千万甚至上亿,考虑内存的优化就比较必要了。

2、关注内存碎片率

  • 内存碎片率是一个重要的参数,对redis 内存的优化有重要意义。
  • 如果内存碎片率过高(jemalloc在1.03左右比较正常),说明内存碎片多,内存浪费严重;这时便可以考虑重启redis服务,在内存中对数据进行重排,减少内存碎片。
  • 如果内存碎片率小于1,说明redis内存不足,部分数据使用了虚拟内存(即swap);由于虚拟内存的存取速度比物理内存差很多(2-3个数量级),此时redis的访问速度可能会变得很慢。因此必须设法增大物理内存(可以增加服务器节点数量,或提高单机内存),或减少redis中的数据。
  • 要减少redis中的数据,除了选用合适的数据类型、利用共享对象等,还有一点是要设置合理的数据回收策略(maxmemory-policy),当内存达到一定量后,根据不同的优先级对内存进行回收。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值