RedisObject
Redis使用对象来表示数据库中的键和值,每次当我们在Redis的数据库中新创建一个键值对时,我们至少会创建两个对象,一个对象用作键值对的键(键对象),另一个对象用作键值对的值(值对象)。
- Redis 数据库中的每个键值对的键和值都是一个对象 redisObject。
typedef struct redisObject {
// 类型(共有 5 种:string、list、hash、set、zset)
unsigned type:4;
// 编码(共有 8 种,两种 int (int、intset),两种 string(embstr、raw),一个压缩列表(ziplist),
// 三种数据结构(linkedlist、skipList、ht)
unsigned encoding:4;
// 指向底层实现数据结构的指针
void *ptr;
// 引用计数
int refcount;
// 最后一次被命令程序访问
unsigned lru:22;
} robj;
类型与编码
- 通过这五种不同类型的对象,Redis可以在执行命令之前,根据对象的类型来判断一个对象是否可以执行给定的命令。使用对象的另一个好处是,我们可以针对不同的使用场景,为对象设置多种不同的数据结构实现,从而优化对象在不同场景下的使用效率。
- Redis的对象系统还实现了基于引用计数技术的内存回收机制,当程序不再使用某个对象的时候,这个对象所占用的内存就会被自动释放。另外,Redis还通过引用计数技术实现了对象共享机制,这一机制可以在适当的条件下,通过让多个数据库键共享同一个对象来节约内存。
- 最后,Redis的对象带有访问时间记录信息,该信息可以用于计算数据库键的空转时长,在服务器启用了maxmemory功能的情况下,空转时长较大的那些键可能会优先被服务器删除。
typedef struct redisObject {
// 类型
unsigned type:4;
// 编码
unsigned encoding:4;
// 指向底层实现数据结构的指针
void *ptr;
// ...
} robj;
类型
- 五种:
- 命令:TYPE
对于Redis数据库保存的键值对来说,键总是一个字符串对象,而值则可以是字符串对象、列表对象、哈希对象、集合对象或者有序集合对象的其中一种,因此:
- 当我们称呼一个数据库键为“字符串键”时,我们指的是“这个数据库键所对应的值为字符串对象”;
- 当我们称呼一个键为“列表键”时,我们指的是“这个数据库键所对应的值为列表对象”。
编码与数据结构
- 对象的ptr指针指向对象的底层实现数据结构,而这些数据结构由对象的encoding属性决定。
- encoding属性记录了对象所使用的编码,也即是说这个对象使用了什么数据结构作为对象的底层实现
每种类型的对象都至少使用了两种不同的编码(即每种类型至少两种实现方式)
- OBJECT ENCODING 对不同编码的输出
类型检查实现
Redis中用于操作键的命令基本上可以分为两种类型:
- 其中一种命令可以对任何类型的键执行,比如说DEL命令、EXPIRE命令、RENAME命令、TYPE命令、OBJECT命令等。
- 而另一种命令只能对特定类型的键执行。
- 注意:对于增加命令来说,因为 redis 没有更新命令,所以可以用来增加和更新,如果是增加的话可以任何类型都可以,如果是更新的话那么会进行检查和多态。
为了确保只有指定类型的键可以执行某些特定的命令,在执行一个类型特定的命令之前,Redis会先检查输入键的类型是否正确,然后再决定是否执行给定的命令。
- 类型特定命令所进行的类型检查是通过redisObject结构的type属性来实现的:
- 在执行一个类型特定命令之前,服务器会先检查输入数据库键的值对象是否为执行命令所需的类型,如果是的话,服务器就对键执行指定的命令;
- 否则,服务器将拒绝执行命令,并向客户端返回一个类型错误。
- 例如:llen
命令多态实现
Redis除了会根据值对象的类型来判断键是否能够执行指定命令之外,还会根据值对象的编码方式,选择正确的命令实现代码来执行命令。
例如:执行 llen
实际上,我们可以将DEL、EXPIRE、TYPE等命令也称为多态命令,因为无论输入的键是什么类型,这些命令都可以正确地执行。
- DEL、EXPIRE等命令和LLEN等命令的区别在于,前者是基于类型的多态——一个命令可以同时用于处理多种不同类型的键,而后者是基于编码的多态——一个命令可以同时用于处理多种不同编码。
对象引用计数
因为C语言并不具备自动内存回收功能,所以Redis在自己的对象系统中构建了一个引用计数(reference counting)技术实现的内存回收机制,通过这一机制,程序可以通过跟踪对象的引用计数信息,在适当的时候自动释放对象并进行内存回收。
实现
由redisObject结构的refcount属性记录
typedef struct redisObject {
// ...
// 引用计数
int refcount;
// ...
} robj;
修改对象引用计数的API
Redis 命令
- 使用OBJECT REFCOUNT命令查看键A的值对象的引用计数
垃圾回收
对象生命周期:可以划分为创建对象、操作对象、释放对象三个阶段。
- 在创建一个新对象时,引用计数的值会被初始化为1;
- 当对象被一个新程序使用时,它的引用计数值会被增一
- 当对象不再被一个程序使用时,它的引用计数值会被减一;
- 当对象的引用计数值变为0时,对象所占用的内存会被释放。
示例:一个字符串对象从创建到释放的整个过程
// 创建一个字符串对象s,对象的引用计数为1
robj *s = createStringObject(...);
// 对象s 执行各种操作...
// 将对象s 的引用计数减一,使得对象的引用计数变为0,导致对象s 被释放
decrRefCount(s)
字符串共享
除了用于实现引用计数内存回收机制之外,对象的引用计数属性还带有对象共享的作用
- 注意:只是共享 int 编码的 0 ~ 10000 字符串对象,还有一些比如包含"OK"回复的字符串对象,包含"ERR"回复的字符串对象等
在Redis中,让多个键共享同一个值对象需要执行以下两个步骤:
- 将数据库键的值指针指向一个现有的值对象;
- 将被共享的值对象的引用计数增一。
Redis int 编码字符串常量池
- 目前来说,Redis会在初始化服务器时,创建一万个字符串对象,这些对象包含了从0到9999的所有整数值,当服务器需要用到值为0到9999的字符串对象时,服务器就会使用这些共享对象,而不是新创建对象。
- 注意:创建共享字符串对象的数量可以通过修改redis.h/REDIS_SHARED_INTEGERS常量来修改。
Redis 不共享其他对象(即除了 int 编码的字符串,和常用字符串比如“OK",其他任何对象均不共享)
- 当服务器考虑将一个共享对象设置为键的值对象时,程序需要先检查给定的共享对象和键想创建的目标对象是否完全相同,只有在共享对象和目标对象完全相同的情况下,程序才会将共享对象用作键的值对象
- 而一个共享对象保存的值越复杂,验证共享对象和目标对象是否相同所需的复杂度就会越高,消耗的CPU时间也会越多:
- 如果共享对象是保存整数值的字符串对象,那么验证操作的复杂度为O(1);
- 如果共享对象是保存字符串值的字符串对象,那么验证操作的复杂度为O(N);
- 如果共享对象是包含了多个值(或者对象的)对象,比如列表对象或者哈希对象,那么验证操作的复杂度将会是O(N 2)。
对象空转时长
除了前面介绍过的type、encoding、ptr和refcount四个属性之外,redisObject结构包含的最后一个属性为lru属性
- 该属性记录了对象最后一次被命令程序访问的时间
实现
typedef struct redisObject {
// ...
// 最后一次被命令程序访问
unsigned lru:22;
// ...
} robj;
Redis 命令
- OBJECT IDLETIME 命令可以打印出给定键的空转时长,这一空转时长就是通过将当前时间减去键的值对象的lru时间计算得出的
- OBJECT IDLETIME命令的实现是特殊的,这个命令在访问键的值对象时,不会修改值对象的lru属性。
maxmemory
键的空转时长还有另外一项作用:
- 如果服务器打开了maxmemory选项,并且服务器用于回收内存的算法为volatile-lru或者allkeys-lru,那么当服务器占用的内存数超过了maxmemory选项所设置的上限值时,空转时长较高的那部分键会优先被服务器释放,从而回收内存。
- 配置文件的maxmemory选项和maxmemory-policy选项的说明介绍了关于这方面的更多信息。
小结
Redis共有字符串、列表、哈希、集合、有序集合五种类型的对象,每种类型的对象至少都有两种或以上的编码方式,不同的编码可以在不同的使用场景上优化对象的使用效率。
- 服务器在执行某些命令之前,会先检查给定键的类型能否执行指定的命令,而检查一个键的类型就是检查键的值对象的类型,然后根据编码确定执行哪种数据结构的函数
Redis的对象系统带有引用计数实现的内存回收机制,当一个对象不再被使用时,该对象所占用的内存就会被自动释放。
- Redis会共享值为0到9999的 int 编码的字符串对象,与常用字符串 比如 “OK”。
对象会记录自己的最后一次被访问的时间,这个时间可以用于计算对象的空转时间。