文章目录
前面讲了许多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_INT | long整数 | 1 | “int” |
REDIS_ENCODING_EMBSTR | embstr编码的简单动态字符串 | 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_STRING | 1,2,3 |
REDIS_LIST | 5和6 |
REDIS_HASH | 8和4 |
REDIS_SET | 4和7 |
REDIS_ZSET | 6和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三种
- 字符串对象是整数值,且可用long类型表示,则将整数值保存在对象的ptr属性中,编码置为int;
- 字符串对象是字符串值,且长度小于等于39,则编码置为embstr,用简单动态字符串保存(SDS);
- 字符串对象是字符串值,且长度大于39,则编码置为raw,使用简单动态字符串保存。
可以看到,如果是字符串,则都使用SDS保存,那么有什么区别呢?
2.1 embstr和raw的区别
raw会调用两次内存分配函数分别创建redisObject
和sdshdr
表示字符串对象,但embstr
只调用一次分配函数分配一块连续的空间,依此包含redisObject
和sdshdr
两个结构。
因此embstr编码是对短字符串的一种优化:
- 分配空间次数降低为一次
- 释放空间也就只需要调用一次释放函数
- 因为数据保存在一块连续的空间,所以可以更好地利用缓存优势
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 编码转换
-
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"
-
embstr转为raw
因为embstr编码字符串对象没有任何修改程序,所以对embstr编码的字符串的修改一定会先转为raw,再执行修改操作,所以对embstr的任何修改操作都会将其转为raw类型
2.3字符串指令的实现
对于int编码,GET
、STRLEN
、GETRANGE
等指令都要先拷贝对象的整数值,再将该整数转为字符串,再计算长度等操作返回。
而APPEND/SETRANGE/
等直接转为raw编码类型字符串,再按raw编码方式执行命令。
对应INCRBY/DECRBY
则直接进行加减操作。INCRBYFLAT
上面讲过。
对应embstr编码的字符串,修改操作都先转为raw编码类型,再执行相关操作,查询等操作直接返回。
对于raw编码的字符串,直接对字符串操作,不用作转换。
embstr和raw的INCRBYFLOAT
都是先尝试转为long double类型,再进行加运算,再将结果转为字符串保存起来。如果不能转为浮点数就报错。
三、列表对象
列表想编码可以是ziplist
或linkedlist
两个。
如果是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编码
- 保存同一键值对的两个节点,键在前,值在后,相邻在一起存储
- 先添加的键值对在表头,后添加的在表尾
如:
> 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属性保存分数。
主要用于zrank
、zrange
等命令。
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设计与实现》