对象
对象
Redis没有直接使用SDS等主要的数据结构来实现数据库,而是根据这些数据结构构建了一个对象系统
这个系统包含5种不同类型的对象:
- 字符串对象
- 列表对象
- 哈希对象
- 集合对象
- 有序集合对象
Redis可以在执行命令前根据对象类型判断是否可以执行给定命令,同时可以针对不同使用场景,为对象设置多种不同的数据结构实现,优化对象在不同场景下的使用效率
Redis的对象系统实现了基于引用计数技术的内存回收机制
同样的,Redis也通过饮用计数技术实现了对象共享机制,可以在适当的条件下让多个数据库键共享一个对象来节约内存
Redis的对象带有访问时间记录信息,可以用于计算数据库键的空转市场。在服务器启用了maxmemory功能的情况下,空转时长较大的键可能被服务器优先删除
对象的类型与编码
数据库的键与值都是用对象表示的。每当创建一个键值对时,至少会创建两个对象
Redis的每个对象都由一个redisObject
结构表示,其中与保存数据有关的3个属性是type
、encoding
、ptr
typedef struct redisObject {
// 类型
unsigned type:4;
// 编码
unsigned encoding:4;
// 指向底层实现数据结构的指针
void *ptr;
// ...
} robj;
类型
type
可以有以下几种类型
类型常量 | 对象名称 |
---|---|
REDIS_STRING | 字符串对象 |
REDIS_LIST | 列表对象 |
REDIS_HASH | 哈希对象 |
REDIS_SET | 集合对象 |
REDIS_ZSET | 有序集合对象 |
对于Redis数据库保存的键值对,键总是一个字符串对象,而值可以是以上五种对象种的任意一种
对一个数据库键执行TYPE命令时,返回的是值对象的类型
编码和底层实现
对象的ptr
指向对象的底层数据结构,而对象的encoding
决定这些数据结构,它记录了对象所使用的编码,其属性可以是以下常量中的一个
每种类型的对象都至少使用了两种不同的编码,以下是每种类型的对象可以使用的编码
使用OBJECT ENCODING命令可以查看一个数据库键的值对象的编码
通过encoding
属性来设定对象所使用的编码,而不是为特定类型的对象关联一种固定的编码,使得Redis的灵活性和效率非常高,Redis可以根据不同的场景为一个对象设置不同的编码,从而优化对象的效率
字符串对象
字符串对象的编码可以是int
、raw
、embstr
如果一个字符串对象保存的是整数值且该整数值可以用long类型来表示,那么该对象会把该值保存在对象的ptr
里面,并将编码设置为int
如果保存的是字符串值且长度大于32字节,那么就会用一个SDS来保存这个字符串值,且将编码设置为raw
如果字符串小于等于32字节,那么字符串对象使用embstr
编码来保存该值
embstr
是专门用于保存短字符串的一种优化编码方式,和raw
一样,使用redisObject
和sdshdr
来表示字符串对象,但是raw
会调用两次内存分配函数来分别创建两个结构,而embstr
则是通过一次内存分配函数来分配一块连续的空间,其中依次包含两个结构
使用embstr
保存短字符串有以下优点
- 创建时内存分配次数只需要1次,内存释放时也只需要1次
- 所有数据都在一块连续的内存中,可以更好地利用缓存带来的优势
浮点数是被当作字符串值来保存在字符串对象里的
编码的转换
int
编码的对象在保存的不再是整数值,而是一个字符串值的时候,将被转换成raw
编码
embstr
编码的字符串对象没有任何相应的修改程序,相当于是只读的,因此在修改embstr
编码的对象时,其编码将会转换成raw
编码
字符串命令的实现
列表对象
列表对象的编码可以是ziplist
或者linkedlist
ziplist
编码使用压缩列表作为底层实现,每个压缩列表节点保存了一个列表元素
linkedlist
编码使用双端链表作为底层实现,每个双端链表的节点保存了一个字符串对象,每个字符串对象保存了一个列表元素
编码转换
列表对象在同时满足以下条件时使用ziplist
编码:
- 列表对象保存的所有字符串元素的长度都小于64字节
- 列表对象保存的元素数量小于512个
不能同时满足这两个条件的列表对象则使用linkedlist
编码
这两个条件的上限值是可以修改的
列表命令的实现
哈希对象
编码可以是ziplist
或者hashtable
使用ziplist
编码时使用压缩列表作为底层实现。每当有新的键值对要加入到哈希对象时,程序会先将保存了键的压缩列表节点推入到列表表尾,然后再把存了值的压缩列表节点推入表尾
因此:
- 同一键值对的两个节点总是紧挨在一起,保存键的节点在前,值的在后
- 先添加到哈希对象的键值对会在表头,后来的会在表尾
使用hashtable
时使用字典作为底层实现,每个键值对都用一个字典键值对来保存,字典的每个键都是一个字符串对象,其保存了键值对的键,而字典的每个值也是一个字符串对象,其保存了键值对的值
编码转换
哈希对象同时满足以下两个条件时使用ziplist
编码:
- 哈希对象保存的所有键值对的键和值的字符串都小于64字节
- 保存的键值对数量小于512个
不能满足这两个条件的哈希对象需要使用hashtable
编码
这两个条件的上限值也可以更改
哈希命令的实现
集合对象
集合对象的编码可以是intset
或者hashtable
使用intset
编码时采用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合里
使用hashtable
编码时采用字典作为底层实现,字典的每个键都是一个字符串对象,每个字符串对象包含一个集合元素,字典的值则为NULL
编码的转换
集合对象同时满足以下两个条件时,使用intset
编码:
- 所有元素都是整数值
- 元素数量不超过512个
否则使用hashtable
编码
第二个条件的上限值可以更改
集合命令的实现
有序集合对象
有序集合对象的编码可以是ziplist
或者skiplist
使用ziplist
编码时采用压缩列表作为底层实现,每个元素使用两个紧挨在一起的压缩列表节点来存储,第一个节点保存元素的成员,第二个元素则保存元素的分值。压缩列表内的集合元素按分值从小到大排序
使用skiplist
编码时采用zset
结构作为底层实现,一个zset
同时包含一个字典和一个跳跃表
typedef struct zset {
zskiplist *zsl;
dict *dict;
} zset;
zset
中的zsl
按分值从小到大保存了所有集合元素,每个跳跃表节点保存了一个集合元素:object
属性保存元素的成员,score
属性保存分值
通过这个跳跃表,程序可以对有序集合进行范围型操作
通过dict
属性,程序可以方便地查找给定成员的分值
有序集合每个元素的成员都是一个字符串对象,每个元素的分值都是一个double类型的浮点数。
此外,zset
中同时使用跳跃表和字典来保存有序集合元素,但是二者是通过指针来共享相同元素的成员和分值的,因此不会产生重复成员和分值,也不会因此浪费额外的内存
编码的转换
当有序集合对象同时满足以下条件时使用ziplist
编码
- 保存的元素数量小于128个
- 所有元素成员的长度小于64字节
不能满足则使用skiplist
编码
这两个条件的上限值也是可以修改的
有序集合命令的实现
有序集合键的所有命令都是针对哈希对象构建的(因为有序集合键的值为哈希对象)
类型检查与命令多态
Redis用于操作键的命令可以分为两种类型。
①可以对任何类型的键执行的命令:
- DEL
- EXPIRE
- RENAME
- TYPE
- OBJECT
等等
②只能对特定类型的键执行的命令:
- SET、GET、APPEND、STRLEN等命令只能对字符串键执行
- HDEL、HSET、HGET、HLEN等命令只能对哈希键执行
- RPUSH、LPOP、LINSERT、LLEN等命令只能对列表键执行
- SADD、SPOP、SINTER、SCARD等命令只能对集合键执行
- ZADD、ZCARD、ZRANK、ZSCORE等命令只能对有序集合键执行
类型检查的实现
执行一个类型特定的命令之前,Redis会先检查输入键的类型是否正确,然后再决定是否执行给定的命令
类型检查是通过redisObject
的type
属性实现的
- 执行类型特定命令之前,服务器先检查输入数据库键的值对象是否为执行命令所需的类型,是则执行命令
- 否则拒绝执行,然后返回一个类型错误
多态命令的实现
Redis除了根据值对象的类型来判断键是否能执行指定命令之外,还会根据值对象的编码方式来选择正确的命令实现代码执行命令
例子:LLEN命令执行的过程
这里是基于编码的多态,诸如DEL等可以对任意类型执行的命令是基于类型的多态
内存回收
Redis在对象系统中构建了一个引用计数技术实现的内存回收机制,通过该机制,程序可以跟踪对象的引用计数信息,并在适当的时候自动释放对象并进行内存回收
每个对象的引用计数信息由redisObject
结构的refcount
属性记录
typedef struct redisObject {
// ...
// 引用计数
int refcount;
// ...
} robj;
对象的引用计数信息会随着对象的使用状态变化:
- 创建新对象时,引用计数的值初始化为1
- 对象被一个新程序使用时,引用计数+1
- 对象不再被一个程序使用时,引用计数-1
- 引用计数为0时,释放对象内存
修改对象引用计数的API:
对象的整个生命周期可以划分为三个阶段:
- 创建对象
- 操作对象
- 释放对象
对象共享
对象的引用计数属性还带有对象共享的作用。
Redis中,让多个键共享同一个值对象需要执行以下两个操作:
- 将数据库键的指针指向一个现有的值对象
- 将被共享的值对象的引用计数+1
Redis在初始化服务器时创建一万个字符串对象,其包含了0-9999的所有整数值,当服务器需要用到这个范围内的字符串对象时,服务器就会使用这些共享对象,而不是创建新对象
创建共享字符串对象的数量可以通过修改redis.h
下的REDIS_SHARED_INTEGERS
常量来修改
这些共享对象不单只有字符串键可以使用,在数据结构中嵌套了字符串对象的对象都可以使用这些共享对象
注意:Redis只共享包含整数值(而非字符串)的对象,这是因为验证两个字符串是否相等的复杂度较高,会带来较大开销,尤其是共享对象是包含了多个值(或对象)的对象
对象的空转时长
redisObject
包含的最后一个属性为lru
属性,该属性记录了对象最后一次被命令程序访问的时间
typedef struct redisObject {
// ...
unsigned lru:22;
// ...
} robj;
OBJECT IDLETIME命令可以打印给定键的空转时长。这个数值是通过当前时间减去目标的lru时间计算得出的。这个命令不会修改对象的lru属性
如果服务器打开了maxmemory属性,且服务器用于回收内存的算法为volatile-lru或者allkeys-lru,那么当服务器占用超过maxmemory时,服务器会优先释放空转时长较高的对象,从而回收内存