34

Redis对象

 3 years ago
source link: https://mp.weixin.qq.com/s?__biz=MzIxNzE1NDA5Mg%3D%3D&mid=2247483849&idx=1&sn=cb0589ba555764432c2b38043a8dd984&chksm=97ff568aa088df9c64cf0b37035091ed72b31294f1805a1d4be2b6800324eca68b4311387a49&token=2128305293&lang=zh_CN%23rd
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Redis对象

原创 林晓 林晓编程 6天前

前言

我们知道redis支持五种数据类型,其实这五种类型就是五种数据对象。我们不曾注意到。其实,实际工作中,我们操作的每一个命令,底层都至少会创建两个对象。一个是键对象、一个是值对象。

今天我们就来学习一下,redis中的五种数据对象是如何工作的,它们又有哪些特性呢?

640?wx_fmt=jpeg

对象的属性

redis 的键和值都是一个对象,每个对象都有以下五个属性:类型、编码、指针、引用计数、空转时长。

typedef struct redisObject {    unsigned type: 4; #类型    unsigned encoding:4; #编码    int refcount; #引用计数    unsigned lru:22; #空转时长    void *ptr; #指向底层实现数据结构的指针    ...}



type 属性,可为以下五种的其中一种:字符串、列表、哈希、集合、有序集合refcount 属性,用于记录该对象被引用的次数,当引用计数为0时,对象会释放lru 属性,用于记录对象最后一次访问的时间,若访问的时间过久,对象会释放ptr 属性,用于指向对象的底层实现的数据结构,而数据结构是由encoding决定的encoding 属性,记录了对象所使用的编码,也就是说,对象底层使用了哪种数据结构作为对象的底层实现。属性值可以是以下表格中的一个。

640?wx_fmt=jpeg

TYPE 命令,可以查看一个数据库键的值对象的类型OBJECT ENCODING 命令,可以查看一个数据库键的值对象的编码

127.0.0.1:6379> set msg 'hello word'OK127.0.0.1:6379> get msg"hello word"127.0.0.1:6379> TYPE msgstring127.0.0.1:6379> OBJECT ENCODING msg"embstr"127.0.0.1:6379>

注意:每种类型的对象都至少使用了两种以上不同的编码方式。通过改变 encoding 的方式,来提高 redis 的灵活性和效率,因为 redis 会根据不同的场景,来为对象设置不同的编码,从而优化对象在某一场景下的效率。

一、字符串对象

字符串的编码可以是以下三种:int、raw、embstr;

int 编码,redis存储的是一个整数值,该整数值可以用long类型表示时,使用int编码。

raw 编码,redis存储的是一个字符串,该字符串长度大于39个字节时,使用raw编码。

embstr 编码,redis存储的是一个字符串,且长度大于等于39个字节时,使用embstr编码。

raw 和 embstr 编码的区别:

raw 会调用两次内存分配机制,内存不是连续空间。释放内存时也需要调用两次函数。

embstr 会调用一次内存分配机制,内存是连续的空间。释放内存只需要调用一次函数。

1.1 编码的转换

因为append命令只允许对 raw 编码的字符串对象进行操作。当我们对一个 int 编码的 或 embstr 编码的字符串对象进行一定的操作时,会将编码转换为 raw 编码的字符串对象。

127.0.0.1:6379> set msg 123OK127.0.0.1:6379> OBJECT ENCODING msg"int"127.0.0.1:6379> append msg 222(integer) 6127.0.0.1:6379> OBJECT ENCODING msg"raw"127.0.0.1:6379>



127.0.0.1:6379> set a 'abc'OK127.0.0.1:6379> object encoding a"embstr"127.0.0.1:6379> append a 123(integer) 6127.0.0.1:6379> object encoding a"raw"127.0.0.1:6379>

以上命令,则是实践 int 和 embstr 编码,通过 append 转换为 raw 编码类型的过程。

二、列表对象

早期的列表对象的编码是由 ziplist 、 linkedlist,也就是说当元素少时使用ziplist,当元素多时使用 linkedlist。但是后来,新版本中对列表数据结构进行了改造,使用 quicklist 代替了 ziplist 和 linkedlist;

quicklist 编码,是使用快速列表作为列表对象的底层实现。ziplist 编码,是使用压缩列表作为列表对象的底层实现。linkedlist 编码,是使用双端链表作为列表对象的底层实现。下面命令,体现出新版本中使用“快速列表”作为底层数据结构的实现

127.0.0.1:6379> lpush list 1(integer) 1127.0.0.1:6379> object encoding list"quicklist"127.0.0.1:6379>

quicklist 是 ziplist 和 linkedlist 的混合体,它将 linkedlist 按段切分,每一段使用 ziplist 存储,多个 ziplist 之间使用双向指针串接起来。如下图所示。

640?wx_fmt=jpeg
  • quicklist 内部默认单个 ziplist 长度为 8k 字节,超出了这个字节数,就会新起一个 ziplist。

  • ziplist 的长度由配置参数 list-max-ziplist-size 决定。

  • 快速列表 默认的压缩深度是0,也就是不压缩。压缩的实际深度可由list-compress-depth 决定。

  • 为了支持快速的 push/pop 操作,quicklist 的首尾两个 ziplist 不压缩,此时深度就是 1。

  • 如果深度为 2,就表示 quicklist 的首尾第一个 ziplist 以及首尾第二个 ziplist 都不压缩。

2.1 快速列表的数据结构、节点

#快速列表struct quicklist {    quicklistNode* head;    quicklistNode* tail;    long count; #元素总数    int nodes; #ziplist 节点的个数    int compressDepth; # LZF 算法压缩深度}
#快速列表节点struct quicklistNode {    quicklistNode* prev;    quicklistNode* next;    ziplist* zl; #指向压缩列表    int32 size; #ziplist 的字节总数    int16 count; #ziplist 中的元素数量    int2 encoding; #存储形式 2bit,原生字节数组还是 LZF 压缩存储}

2.2 双端链表、压缩列表、快速列表的特点

双端链表,进行push、pop来说速度很快,但是它的内存开销比较大,因为它要额外存储前驱指针和后继指针。链表采用不连续的内存空间,所以它对修改和插入操作来说相当快速,但是容易产生内存碎片。

压缩列表,仅限于存储字节较短的数据,因为它是为节省内存而开发的数据结构,压缩列表使用连续的内存空间,所以它对删除和插入的效率相对较低,但是查询效率很高。压缩列表对于插入很大的数据情况下,会产生扩容和大量拷贝流程,此时效率相对较低。

快速列表是对压缩列表和双端链表的空间和时间效率的折中产物。它结合了二者的优点。

三、哈希对象

哈希对象的编码方式有 ziplist 、hashtable

ziplist 编码,哈希对象底层使用压缩列表作为底层实现。

hashtable 编码,哈希对象底层使用字典作为底层实现。

下面命令,是压缩列表实现的哈希对象存储。

127.0.0.1:6379> hset mytest name "cici"(integer) 1127.0.0.1:6379> hset mytest age 18(integer) 1127.0.0.1:6379> hset mytest sex '女'(integer) 1127.0.0.1:6379> object encoding mytest"ziplist"127.0.0.1:6379>
640?wx_fmt=jpeg

下面命令,是字典实现的哈希对象存储。

127.0.0.1:6379> hset mytest content '我是中国人,我在测试哈希,你看到了吗?我是中国人,我在测试哈希,你看到了吗?我是中国人,我在测试哈希,你看到了吗?我是中国人,我在测试哈希,你看到了吗?我是中国人,我在测试哈希,你看到了吗?我是中国人,我在测试哈希,你看到了吗?我是中国人,我在测试哈希,你看到了吗?'127.0.0.1:6379> object encoding mytest"hashtable"127.0.0.1:6379>

当 ziplist 作为哈希对象的底层实现时,若再次存储键值,长度大于等于64字节时,哈希对象的底层实现,会进行编码转换,此时的哈希对象的编码方式为 hashtable 。

640?wx_fmt=jpeg

如上图所示,hashtable 编码的哈希对象,使用字典作为底层实现,哈希对象中的每个键值对都使用一个字典键值对来存储。字典的每个键和值都是一个字符串对象,对象中分别存存了键值对的键和值。

3.1 编码转换

当哈希对象同时满足以下两个条件时,默认情况下哈希对象会使用压缩 ziplist 编码;

  • 哈希对象保存的所有键值对的键和值长度小于64字节

  • 哈希对象保存的键值对数量小于 512 个

以上两个条件可以通过配置文件进行修改。若不满足以上两个条件的情况下,将会采用 hashtable 进行编码。

四、集合对象

集合对象的编码有 intset、hashtable

intset 编码,集合对象使用整数集合作为底层实现,集合元素都存储在整数集合里。hashtable 编码,集合对象使用字典作为底层实现。下面命令,是整数集合实现的集合对象存储。

127.0.0.1:6379> sadd mynumber 111 222 333(integer) 3127.0.0.1:6379> object encoding mynumber"intset"127.0.0.1:6379>
640?wx_fmt=jpeg

下面命令,是字典实现的集合对象存储

127.0.0.1:6379> sadd mynumber "hello" "你好"(integer) 2127.0.0.1:6379> object encoding mynumber"hashtable"127.0.0.1:6379>

下面命令,是查看集合中的元素数量 和 元素值,由此可见,集合编码发生了变化,已经从 intset 编码转换成 hashtable 编码。

127.0.0.1:6379> scard mynumber(integer) 5127.0.0.1:6379> smembers mynumber1) "hello"2) "333"3) "111"4) "222"5) "\xe4\xbd\xa0\xe5\xa5\xbd"127.0.0.1:6379>
640?wx_fmt=jpeg

由上图可见,集合中因为添加了字符串类型值,因此从原本的 intset 编码转为 hashtable 编码。

4.1 编码转换

当集合对象同时满足以下两个条件时,对象会使用 intset 编码

  • 集合对象保存的所有元素都是整数值。

  • 集合对象保存的元素数量不超过 512 个。

以上两个条件是可以修改的,通过修改配置文件即可。若不满足以上条件的集合对象将使用 hashtable 编码。

五、有序集合对象

有序集合对象是有 ziplist 、skiplist

ziplist 编码,有序集合对象使用压缩列表作为底层实现。

skiplist 编码,有序集合对象使用 zset 结构作为底层实现,一个zset包含一个字典和一个跳跃表。

下面命令,是压缩列表作为有序集合对象的底层实现。

127.0.0.1:6379> zadd price 5.6 apple 3.4 orange(integer) 2127.0.0.1:6379> object encoding price"ziplist"127.0.0.1:6379>
640?wx_fmt=jpeg

注意:有序集合在压缩列表中按分值大小进行排序存储。

下面命令,是跳跃表作为有序集合对象的底层实现,它是一个 zset 结构。

127.0.0.1:6379> zadd price 4.4 "买了一件衣服,这个衣服真好看,真的很好看,真的很好看,真的很好看"(integer) 1127.0.0.1:6379> object encoding price"skiplist"127.0.0.1:6379>

下面命令,是查看有序集合的元素个数和排序情况。

127.0.0.1:6379> zcard price(integer) 3127.0.0.1:6379> ZRANGE price 0 -11) "orange"2) "\xe4\xb9\xb0\xe4\xba\x86\xe4\xb8\x80\xe4\xbb\xb6\xe8\xa1\xa3\xe6\x9c\x8d\xef\xbc\x8c\xe8\xbf\x99\xe4\xb8\xaa\xe8\xa1\xa3\xe6\x9c\x8d\xe7\x9c\x9f\xe5\xa5\xbd\xe7\x9c\x8b\xef\xbc\x8c\xe7\x9c\x9f\xe7\x9a\x84\xe5\xbe\x88\xe5\xa5\xbd\xe7\x9c\x8b\xef\xbc\x8c\xe7\x9c\x9f\xe7\x9a\x84\xe5\xbe\x88\xe5\xa5\xbd\xe7\x9c\x8b\xef\xbc\x8c\xe7\x9c\x9f\xe7\x9a\x84\xe5\xbe\x88\xe5\xa5\xbd\xe7\x9c\x8b"3) "apple"127.0.0.1:6379>
640?wx_fmt=jpeg

由上图可见,当有序集合使用了 skiplist 编码方式,其实底层采用了zset结构来存储了数据内容。

zset 结构分别用了一个字典 和 一个跳跃表来完成底层实现。

字典的优点,在于它以时间复杂度为O(1) 的速度取值。

跳跃表的优点,在于它以分值进行从小到大的排序。结合二者的优点作为 zset 的整体结构来完成了有序集合的底层实现。

5.1 编码的转换

有序集合对象同时满足以下两个条件时,对象使用ziplist 编码

  • 有序集合保存的元素数量小于128个。
  • 有序集合保存的所有元素成员长度都小于64字节。

以上两个条件是可以修改的,通过修改配置文件即可。若不满足以上条件的有序集合对象将使用 skiplist 编码。

六、多态命令的实现

前面我们说过,对象包含类型、编码、指针、引用计数、空转时长 五个属性。编码在和指针在前面已经分别阐述了具体的实现方式。那类型主要用于哪方面呢?

其实,类型主要用于判断用户执行一个命令时,检测命令的键是否能够执行该命令。如果可以执行,则服务器就对键执行指定的命令,否则,服务器会拒绝执行命令,并抛出一个异常。

多态主要的体现,是当一个对象的底层实现,不止一种编码方式时,需要根据编码方式进行区分,来选择正确的命令执行方式。

640?wx_fmt=jpeg

七、引用计数、空转时长

1)Redis在自己的对象系统中,构建了一个对象引用计数,可用于内存回收机制,也可实现对象的共享机制,让多个数据库共享一个对象,用于节约内存的作用。

2)对象带有访问时间记录信息,用于删除空间转时间较大的那个键。

八、重点回顾

本文主要学习到,redis的五大数据对象,我们知道redis中的每个键值对其实都是一个对象,而每个对象的底层实现都至少采用两种以上的编码方式来提高redis的性能和效率。另外,redis还支持引用计数和内存共享机制。用于提高对象的最大化利用和快速释放无用对象。当我们执行每一个redis命令时,redis都会首先检测该命令的键是否支持执行该命令,若不支持,则报类型异常。

九、参考文献

《Redis设计与实现》

《Redis深度历险》

640?wx_fmt=gif

640?wx_fmt=gif


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK