前言
之前我们说了 Redis 的基本数据结构,但是 Redis 并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统。
除此之外,Redis还引入了基于引用计数技术的内存回收机制和键的空转时长
对象的类型和编码
Redis 使用对象来表示数据库中的键和值,当我们在 Redis 数据库中创建一个键值对时,键默认是字符串对象,值可以是字符串、链表、集合、哈希、有序集合。
Redis中每个对象都是由一个 RedisObject 结构表示 ,改结构中有三个和和保存的数据相关的属性,type、encoding、ptr
1 | struct redisObject{ |
- type:属性记录了对象的类型,其值可以为下面几种
类型常量 | 对象的名称 |
---|---|
REDIS_STRING | 字符串对象 |
REDIS_LIST | 列表对象 |
REDIS_HASH | 哈希对象 |
REDIS_SET | 集合对象 |
REDIS_ZSET | 有序集合对象 |
我们可以通过 TYPE 命令来查看数据库键对应的值对象的类型
- ptr:指针指向对象的底层实现的数据结构,而这些数据结构由对象的 encoding 属性决定
- encoding:记录了对象所使用的编码,也即这个对象使用了什么数据结构作为对象的底层实现,其值可以为下面几种
编码常量 | 编码对应的底层数据结构 |
---|---|
REDIS_ENCODING_INT | long 类型的编码 |
REDIS_ENCODING_EMBSTR | embstr编码的简单动态字符串 |
REDIS_ENCODING_RAM | 简单动态字符串 |
REDIS_ENCODING_HT | 字典 |
REDIS_ENCODING_LINKEDLIST | 双端链表 |
REDIS_ENCODING_ZIPLIST | 压缩列表 |
REDIS_ENCODING_INTSET | 整数集合 |
REDIS_ENCODING_SKIPLIST | 跳跃表和字典 |
我们可以使用 OBJECT ENCODING 命令可以查看一个数据库键
字符串对象
字符串对象的编码可以是 int、raw、embstr
如果一个字符串对象保存的是整数值,且这个整数可以用 long 类型来表示,那么字符串对象会将整数值保存在字符串对象结构的 ptr 属性里面,将 void* 转换成 long ,将字符串对象的编码设置为 int
如果字符串对象保存的字符串值的长度大于39字节,那么字符串对象会使用简单动态字符串(SDS)来保存这个字符串值,并将对象的编码设置为 raw
如果字符串对象保存的字符串值长度小于39字节,那么字符串对象将使用 embstr 编码的方式来保存这个字符串值。
raw和embstr的区别
- embstr编码将创建字符串对象所需的内存分配次数从 raw 的两次降低为一次
- 释放 embstr 编码字符串对象只需要调用一次内存释放函数,而 raw 编码的字符串需要两次
- embstr 编码的字符串对象的所有数据都保存在一块连续的内存里面,所以这比 raw 编码的字符串对象能更好的利用缓存带来的优势
int编码的字符串对象和 embstr 编码的字符串对象在条件满足的情况下,会被转换成 raw 编码的字符串对象。因为Redis没有为embstr编码的字符串对象编写任何相应的修改程序,所以embstr编码的字符串对象实际上是只读的。
列表对象
列表对象的编码可以是 ziplist 或者 linkedlist
ziplist 编码的列表对象使用压缩列表作为底层实现,每个压缩列表节点保存了一个列表元素
linkedlist 编码的列表对象使用双端链表作为底层实现,每个双端链表节点都保存了一个字符串对象,而每个字符串对象都保存了列表元素
当列表对象同时满足以下两个条件时,列表对象使用 ziplist 编码
- 列表对象保存的所有字符串元素的长度都小于64字节
- 列表对象保存的元素数量小于512个;不能满足这两个条件的列表对象需要使用 linkedlist 编码
哈希对象
哈希对象的编码可以是 ziplist(REDIS_ENCODING_ZIPLIST) 和 hashtable(REDIS_ENCODING_HT)
ziplist 编码的哈希对象使用压缩列表作为底层实现,每当有新的键值对加入到哈希对象时,程序会先将保存了键的压缩列表节点推入压缩列表表尾,然后再将保存了值的压缩列表节点推入到压缩列表表尾。
- 保存了同一键值对的两个节点总是挨在一起,保存键的节点在前,保存了值的节点在后
- 先添加到哈希对象中的键值对会被放在压缩列表的表头方向,而后来添加到的会被放在压缩列表的表尾方向
hashtable 编码的哈希对象使用字典作为底层实现,哈希对象中的每个键值对都使用一个字典键值对来保存
- 字典的每个键都是一个字符串对象,对象中保存了键值对的键;
- 字典的每个值都是一个字符串对象,对象中保存了键值对的值
当哈希对象同时满足以下两个条件时,哈希对象使用ziplist编码
- 哈希对象保存的所有键值对的键和值的字符串长度都小于 64 字节
- 哈希对象保存的键值对数量小于 512 个;不能满足这两个条件的哈希对象需要使用 hashtable
集合对象
集合对象的编码可以是 intset(REDIS_ENCODING_INTSET) 或者 hashtable
inset 编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合里面。
hashtable 编码的集合对象使用字典作为底层实现,字典的每个键都是一个字符串对象,每个字符串对象包含了一个集合元素,而字典的值则全部被设置为null
当集合对象同时满足以下条件时,对象使用 intset 编码
- 集合对象保存的所有元素都是整数值
- 集合对象保存的元素数量不超过 512 个
有序集合对象
有序集合的编码可以是 ziplist 或者 skiplist
ziplist编码的有序集合对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点保存,第一个节点保存元素的成员,而第二个元素则保存元素的分值
集合元素按照分值由大到小排序,分值较小的元素被放置在靠近表头的位置,而分值较大的被放置在靠近表尾的位置
当有序集合对象可以同时满足以下两个条件时,对象使用ziplist编码
- 有序集合保存的元素数量小于128个
- 有序集合保存的所有元素成员长度都小于64字节
内存回收
C语言并没有具备自动回收的功能,所以在Redis内部,Redis自己构建了一个基于引用计数的内存回收标准。程序可以通过跟踪对象的引用技术信息,在适当的时候自动释放对象并进行内存回收。
- 当创建一个新对象时,引用计数的值会被初始化为1
- 当对象被一个新程序使用时,它的新技术会增1
- 当对象不再被一个程序使用时,它的引用计数会减1
- 当对象的引用计数变成0时,对象所占用的内存会被释放
对象共享
对象的引用计数属性除了可以用来进行内存回收之外,也可以用来进行对象的共享。Redis在初始化服务器的时候,会创建1万个字符串对象,包含了从0到9999的所有整数值,当服务器需要用到0到9999的字符串对象时,服务器就会使用这些共享对象,而不再去创建新对象。
Redis只对包含整数值的字符串对象进行共享对象的空转时长
前面介绍了 RedisObject 结构包含的四个属性,type、encoding、ptr、refcount;除了这些最后再介绍一个属性 lru 属性,改属性记录了对象最后一次被命令程序访问的时间。
键的空转时长可以用于回收内存,当服务器打开了 maxmemory 选项,并且服务器用于回收内存的算法为 volatile-lru 或者 allkeys-lru ,那么当服务器占用的内存数超过了 maxmemory 选项设置的上限,空转时长较高的那部分键会优先被服务器释放,从而回收内存。