Redis设计与实现(五)——Redis五种对象、内存回收/共享


前面讲了许多redis用到的数据结构,但是实际上并没有直接使用这些数据结构,而是基于这些数据结构创建了一个对象系统: 字符串对象、列表对象、哈希对象、集合对象和有序集合对象。

redis基于引用计数法实现内存回收机制,即某个对象被用到一次,则计数器加1,当计数器为0时,表示该对象没有被引用,则就会被自动释放;

同时也通过引用计数实现对象共享机制,在某些条件下让多个数据库键共享同一个对象。

一、对象类型和编码

redis中的对象由redisObject结构表示:

typedef struct redisObject{
    //类型
    unsigned type:4;
    //编码
    unsigned encoding:4;
    //指向底层实现数据结构的指针;
    void *ptr;
    //引用计数
    int refcount;
    //对象最后一次被命令程序访问的时间
    unsigned lru:22;
    //....
} robj;

1.1 类型

type属性记录对象类型。

对象类型常量TYPE命令输出
字符串对象REDIS_STRING“string”
列表对象REDIS_LIST“list”
哈希对象REDIS_HASH“hash”
集合对象REDIS_SET“set”
有序集合对象REDIS_ZSET“zset”

redis中的键总是字符串对象,值可以是上面5种中的任意一种。

1.2 编码和底层实现

对象的ptr指针指向对象的底层实现数据结构,而这些数据结构由对象的encoding属性决定。

encoding编码决定了对象的底层实现。

编码常量对应的底层数据结构序号OBJECT ENCODING命令的输出
REDIS_ENCODING_INTlong整数1“int”
REDIS_ENCODING_EMBSTRembstr编码的简单动态字符串2“embstr”
REDIS_ENCODING_RAW简单动态字符串3“raw”
REDIS_ENCODING_HT字典4“hashtable”
REDIS_ENCODING_LINKEDLIST双向链表5“linkedlist”
REDIS_ENCODING_ZIPLIST压缩列表6“ziplist”
REDIS_ENCODING_INTSET整数集合7“intset”
REDIS_ENCODING_SKIPLIST跳表和字典8“skiplist”

每个类型对象可以使用至少两种编码:

类型对应编码序号(上表)
REDIS_STRING1,2,3
REDIS_LIST5和6
REDIS_HASH8和4
REDIS_SET4和7
REDIS_ZSET6和8

因此redis的数据类型通过encoding属性决定,是灵活多变的,可以根据不同的情况使用合适的数据结构存储。

如数据少时,使用压缩列表,保证数据占用空间为连续的,可以更快被载入内存,而数据多时,就可以改为双端队列来存储,适合存储大量元素。

1.3 内存回收

redisObject中的refcount属性用于引用计数:

  • 创建一个新对象,计数+1
  • 对象被新程序引用,计数+1
  • 对象不再被新程序引用,计数-1
  • 引用为0时,对占用内存进行回收

1.4 对象的共享

通过引用计数,可以实现对象的共享。

但redis只对包含整数值的字符串对象进行共享(因为对于字符串值的或是多个值的对象,进行匹配验证是否相同会消耗大量的CPU,虽然节省内存,但验证太耗时,因此不进行共享)。

如键A创建了一个整数值1的字符串对象。

此时如果B也创建了一个保存整数值1的字符串对象,则键A和键B共享同一个值对象,键B不会创建新的值对象。

同时值1的字符串对象的refcount加1。

redis会在初始化服务器时创建1万个字符串对象,从0到9999的所有整数值,所以用到0到9999的字符串对象时不会创建新的对象。

1.5对象空转时长

空转时长,即当前时间减去对象的lru时间。

通过OBJECT IDLETIME命令查看:

127.0.0.1:6379> set msg wml
OK
127.0.0.1:6379> object idletime msg
(integer) 14
127.0.0.1:6379> get msg
"wml"
# 访问该对象后,立即查看空转时长
127.0.0.1:6379> object idletime msg
(integer) 1

另外,配置文件中如果使用了maxmemory选项,且内存回收算法为volatile-lru或allkeys-lru,则当内存占用超过了maxmemory时,就会优先将空转时较高的键进行释放回收内存。

# maxmemory <bytes>
# 默认未打开,单位字节

二、字符串对象

根据上面讲的,字符串对象对应的编码可以是:int,embstr,raw三种

  1. 字符串对象是整数值,且可用long类型表示,则将整数值保存在对象的ptr属性中,编码置为int;
  2. 字符串对象是字符串值,且长度小于等于39,则编码置为embstr,用简单动态字符串保存(SDS);
  3. 字符串对象是字符串值,且长度大于39,则编码置为raw,使用简单动态字符串保存。

可以看到,如果是字符串,则都使用SDS保存,那么有什么区别呢?

2.1 embstr和raw的区别

raw会调用两次内存分配函数分别创建redisObjectsdshdr表示字符串对象,但embstr只调用一次分配函数分配一块连续的空间,依此包含redisObjectsdshdr两个结构。

因此embstr编码是对短字符串的一种优化:

  1. 分配空间次数降低为一次
  2. 释放空间也就只需要调用一次释放函数
  3. 因为数据保存在一块连续的空间,所以可以更好地利用缓存优势

2.2 long double类型的存储

对应long double类型表示的浮点数,也是作为字符串值保存的。

对于以下命令:

127.0.0.1:6379> set score 88.8
OK
127.0.0.1:6379> OBJECT ENCODING score
"embstr"
127.0.0.1:6379> incrbyfloat score 10
"98.8"
127.0.0.1:6379> OBJECT ENCODING score
"embstr"

redis会先将浮点数88.8转为字符串,再将该字符串保存起来。

如果调用incrbyfloat命令,则先将该字符串转为浮点数,做增加操作后,再将结果转为字符串保存起来。

2.3 编码转换

  1. int转raw

    当给字符串为整数的值添加字符串值时,就会从int转为raw

    127.0.0.1:6379> set key1 2
    OK
    127.0.0.1:6379> OBJECT ENCODING key1
    "int"
    127.0.0.1:6379> append key1 w
    (integer) 2
    127.0.0.1:6379> OBJECT ENCODING key1
    "raw"
    
    
  2. embstr转为raw

    因为embstr编码字符串对象没有任何修改程序,所以对embstr编码的字符串的修改一定会先转为raw,再执行修改操作,所以对embstr的任何修改操作都会将其转为raw类型

2.3字符串指令的实现

对于int编码,GETSTRLENGETRANGE等指令都要先拷贝对象的整数值,再将该整数转为字符串,再计算长度等操作返回。

APPEND/SETRANGE/等直接转为raw编码类型字符串,再按raw编码方式执行命令。

对应INCRBY/DECRBY则直接进行加减操作。INCRBYFLAT上面讲过。

对应embstr编码的字符串,修改操作都先转为raw编码类型,再执行相关操作,查询等操作直接返回。

对于raw编码的字符串,直接对字符串操作,不用作转换。

embstr和raw的INCRBYFLOAT都是先尝试转为long double类型,再进行加运算,再将结果转为字符串保存起来。如果不能转为浮点数就报错。

三、列表对象

列表想编码可以是ziplistlinkedlist两个。

如果是ziplist,则redis对象的ptr指向一个压缩列表对象,而如果是linkedlist,ptr指向一个双端链表,且每个链表节点都保存了一个字符串对象,每个字符串对象保存了一个列表元素。

编码的转换

下面两个条件任意一个不满足,则从ziplist转换为linkedlist

  • 列表对象保存的所有字符串长度都小于64字节
  • 列表对象的元素数量小于512个
127.0.0.1:6379> RPUSH list2 wml1 wml2 wml3
(integer) 3
127.0.0.1:6379> OBJECT ENCODING list2
"quicklist"

但是测试了下发现并不是ziplist,而是quicklist,这是为啥?

其实书上用的版本是3.0的,而在3.2后,增加了一个快速列表的结构quicklist,该结构综合了ziplist和linkedlist,是一个双端的压缩列表。

quicklist

它的结构如下:

在这里插入图片描述

图片来源:https://www.cnblogs.com/hunternet/p/12624691.html


typedef struct quicklist {
    //头结点
    quicklistNode *head; 
    //尾节点
    quicklistNode *tail; 
    //entry总数
    unsigned long count; 
    //节点quicklistNodes 个数
    unsigned int len;   
    //ziplist中entry能保存的数量,由list-max-ziplist-size配置项控制 
    int fill : 16;       
    //压缩深度,由list-compress-depth配置项控制
    unsigned int compress : 16; 
} quicklist;

quicklistNode

typedef struct quicklistNode {
    //前节点指针
    struct quicklistNode *prev; 
    //后节点指针
    struct quicklistNode *next; 
    //数据指针。当前节点的数据没有压缩,那么它指向一个ziplist结构;否则,它指向一个quicklistLZF结构。
    unsigned char *zl;
    //表示zl指向的ziplist实际占用内存大小。但即使ziplist被压缩了,该值仍然是压缩前的ziplist大小
    unsigned int sz;  
    //ziplist里面包含的数据项个数
    unsigned int count : 16;   
    //ziplist是否压缩。取值:1:未被压缩 2:被压缩了
    unsigned int encoding : 2; 
    //存储类型,固定值2,表示使用ziplist存储
    unsigned int container : 2; 
    //当我们使用类似lindex这样的命令查看了某一项本来压缩的数据时,需要把数据暂时解压,这时就设置recompress=1做一个标记,等有机会再把数据重新压缩
    unsigned int recompress : 1;
    unsigned int attempted_compress : 1; /* node can't compress; too small */
    //其他扩展字段
    unsigned int extra : 10;
} quicklistNode;

quicklistLZF

表示被压缩的ziplist

typedef struct quicklistLZF {
    //压缩后的ziplist大小
    unsigned int sz; 
    //柔性数组,压缩后的ziplist字节数组
    char compressed[];
} quicklistLZF;

quicklist综合了ziplist和linkedlist的优点,空间和时间也做了中和。

四、哈希对象

哈希对象编码为ziplist或hashtable

ziplist编码

  1. 保存同一键值对的两个节点,键在前,值在后,相邻在一起存储
  2. 先添加的键值对在表头,后添加的在表尾

如:

> hmset test k1 v1 k2 v2 k3 v3
OK

在这里插入图片描述

hashtable编码

哈希对象的每个键值对都使用一个字典键值对保存,每个键和值都使用一个字符串对象保存键和值。

在这里插入图片描述

编码转换

同时满足下面两个情况使用ziplist编码:

  • 哈希对象的每个键值对的键和值的字符串长度都小于64字节
  • 键值对总个数小于512个

可在配置文件中修改这两个参数:

hash-max-ziplist-entries 512
hash-max-ziplist-value 64

示例:

127.0.0.1:6379> hset book name java
(integer) 1
127.0.0.1:6379> OBJECT ENCODING book
"ziplist"
127.0.0.1:6379> hset book haha wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww

(integer) 1
127.0.0.1:6379> OBJECT ENCODING book
"hashtable"

五、集合对象

编码可为intset或hashtable

intset编码

整数集合作为底层数据结构。

所有元素保存在整数集合里。

hashtable编码

字典作为底层实现。

每个 键都是一个字符串对象,每个字符串对象包含一个集合员孙,字典的值全部为NULL。

编码转换

以下条件满足,才能使用intset编码:

  • 所有元素都是整数值
  • 元素个数小于512个

第二个条件可在配置中修改:

set-max-intset-entries 512

六、有序集合对象

编码可为ziplist或skiplist

ziplist编码

同哈希对象的ziplist编码相似,只是这里一个元素和它的分数是相邻在一起的,且元素在前,分数在后,按照分数大小从表头到表尾排序。

skiplist编码

skiplist编码底层实现为一个zset结构:

typedef struct zset{
    zskiplist *zsl;
    dict *dict;
} zset;

可以看到,zset中使用了一个跳表和一个字典来实现。

zskiplist

正常的跳表结构,按分值从小到大保存集合元素,跳表节点的object属性保存元素值,score属性保存分数。

主要用于zrankzrange等命令。

dict

主要用于可以在O(1)的时间内查找给定元素的分数。

字典的键存储元素的值,字典的值存储元素的分数。

注意

虽然zset同时使用跳跃表和字典实现,但两种数据结构会通过指针共享相同元素的成员和分值,因此不会产生任何重复的成员和分值,也不会造成内存浪费。

同时使用跳跃表和字典主要是为了让有序集合中的各个命令都能够达到最优的执行效率。

编码转换

同时满足以下条件才使用ziplist编码:

  • 元素总数小于128个
  • 每个元素长度都小于64字节

可在配置文件中修改这两个参数:

zset-max-ziplist-entries 128 
zset-max-ziplist-value 64
127.0.0.1:6379> zadd zset1 1 k1 4 k2 3 k3
(integer) 3
127.0.0.1:6379> object encoding zset1
"ziplist"
127.0.0.1:6379> zadd zset1 9 wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww
(integer) 1
127.0.0.1:6379> object encoding zset1
"skiplist"

参考:《Redis设计与实现》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值