类型检查和多态命令的实现
redis中用于键操作的命令基本上可以分为两类:
可以对任何类型的键执行, eg.
del
,expire
,rename
,type
,object
只能对特定命令执行的键,
eg.
set
、get
、append
、strlen
等命令只能对字符串键执行
hdel
、hset
、hget
、hlen
等命令只能对hash键执行
rpush
、lpop
、linsert
、llen
等只能对列表键执行
sadd
、spop
、sinter
、scard
等命令只能对集合键执行
zadd
、zcard
、zrank
、zcore
等命令只能对有序集合键执行
类型检查的实现
类型特定命令所进行的类型检查是通过redisObject
结构的type属性
来实现的.
- 在执行一个类型特定命令之前, 服务器先检查输入数据库键的值对象是否为执行命令所需要的类型, 是、就执行
- 否则, server拒绝执行、并向client返回一个类型错误
eg. 对于llen命令:
在执行llen命令前、server会先检查输入数据库键的值对象是否为列表类型
, 即: 检查redisObject
的type属性
是否为redis_list
, 是的话、执行 llen命令, 否则返回类型错误
多态命令的实现
Redis除了会根据值对象的类型来判断是否能执行特定命令外、还会根据值对象的编码方式、选择正确的命令实现代码来执行命令
eg. 对一个键执行 llen
命令, 则服务器除了要确保执行命令的是列表键之外, 还要根据键的值对象所使用的编码来选择正确的llen命令实现
- 若列表对象的编码为 ziplist, 那么说明列表对象的实现为压缩列表, 程序将使用 ziplistLen 函数来返回列表的长度
- 若列表对象的编码为 linkedlist, 说明列表对象的实现为双端链表, 程序将使用 listLength 函数来返回列表的长度
借用面向对象的术语来说、可以认为llen命令的实现是多态的, 只要执行 llen 命令的是列表键、无论值对象是 ziplist 还是 linkedlist 编码、命令都可以正常执行
del
、expire
等命令和llen
命令的区别在于、前者是基于类型的多态, 一个命令可以同时处理多种不同类型的键、而后者是基于编码的多态: 一个命令可以同时用于处理多种不同的编码
内存回收
因为C语言并不具备内存回收功能, redis 在自己的对象系统中构建了一个引用计数(reference counting) 技术来实现内存回收机制, 通过引用计数机制、程序可以通过跟踪对象的引用计数信息、在适当的时候自动释放对象并进行内存回收
每个对象的引用计数信息由 RedisObject 结构的 refcount属性
记录:
typedef struct redisObject {
// ...
int refcount; // 引用计数
// ...
} robj;
对象的引用技术信息会随着对象的使用状态不断变化
- 创建一个新的对象时、引用计数初始化为1
- 对象被一个新的程序引用时、引用计数值 +1
- 对象不再被一个程序引用时、引用计数值 -1
- 对象的引用计数值变为0时、对象所占用的内存会被释放
下边是修改对象引用计数的API
函数 | 作用 |
---|---|
incrRefCount | 将对象的引用计数值+1 |
decrRefCount | 将对象的引用计数值-1, 当对象的引用计数值=0时、释放对象 |
resetRefCount | 将对象的引用计数值设为0, 但不释放对象、需要重设对象引用值是使用 |
其它不同类型的对象也会经历类似的过程
共享对象
除了实现引用计数内存回收机制外、对象的引用计数属性还带有对象共享的作用.
eg. A键创建了一个包含整数值100的字符串对象作为值对象, 此时若B键也想要创建一个同样保存了整数值100的字符串对象作为值对象、那么Server有两种做法:
- 为键B创建一个包含整数值100的字符串对象
- 让键A和键B共享同一个字符串对象
明显, 第二种方式更节约内存, 在Redis中、多个键共享同一个值需要执行以下步骤:
- 将数据库键的值指向一个现有的值对象
- 将被共享的值对象的引用计数+1
**注意: **
创建共享字符串对象的数量可以通过修改 redis.h/redis_shared_integers
常量来修改
eg, 创建一个值为100的键a, 使用object refcount
命令查看a的引用计数, 会发现值为2
redis> set a 100
OK
redis> object refcount a
(integer) 2
引用这个值对象的两个程序分表是持有这个值对象的服务器程序, 及共享这个值对象的键A
另外: 这些共享对象不单单只有字符串键可以使用, 那些在数据结构中嵌套了字符串对象的对象(linkedlist编码的列表对象、hashtable编码的hash对象、hashtable编码的集合对象及zset编码的有序集合对象)等都可以使用这些共享对象
思考
为什么redis不共享包含字符串的对象?
当服务器考虑将一个共享对象设置为键的值对象时、程序需要检查给定的共享对象和键想创建的目标对象是否完全相同, 只有在共享对象和目标对象完全相同的情况下、程辉才会将共享对象的用作键的值对象、而一个共享对象保存的值越复杂、验证两者相同的复杂度就会越高, 消耗的CPU时间也会越多
- 若共享对象保存整数值的字符串对象、那么验证操作的复杂度为 O(1)
- 若共享对象是保存字符串值的字符串对象、那么验证操作的复杂度为 O(N)
- 若共享对象是包含了多个值(或者对象)的对象, 比如列表对象或者hash对象、验证的复杂度将是O(N²)
因此、尽管共享更复杂的对象可以节约更多内存、但受到CPU时间的限制、redis只对包含整数值的字符串对象进行共享
对象的空转时长
除了前边介绍过的type
、encoding
、ptr
和 refcount
4个属性外, redisObject结构包含的最后一个属性为 lru属性
, 它记录了对象最后一次被命令访问的时间
typedef struct redisObject {
// ...
unsigned lru:22;
// ...
} robj;
object idletime
命令可以打印出给定键的空转时长, 就是通过当前时间 - 键的lru时间得到的
注意:
Object idletime
的实现是特殊的, 它在访问键的时候、不会修改值对象的lru属性
除了使用 命令打印键的空转时长, lru属性还用于回收内存, 当设置了 maxmemory 选项, 且服务器用于回收内存的算法为 volatile-lru
或者 allkeys-lru
时、当服务器占用内存超过了 maxmemory设置的上限值时, 空转时长较高的键会优先被服务器释放.
重点回顾
- redis数据库的中每个键值对的键和值都是一个对象
- redis共有字符串、列表、hash、结合、有序集合五种类型的对象, 每种类型的对象至少有2种或以上的编码方式, 不同的编码可以在不同的场景上优化对象的使用概率
- 服务器在执行某些命令之前、会先检查给定键的类型能否执行
- redis的对象系统带有引用计数实现的内存回收机制, 当一个对象不再被使用时、该对象占用的内存会被自动释放
- redis会共享值为 0 到 9999 的字符串对象
- 对象会记录自己最后一次被访问的时间, 这个时间还可以用于计算对象的空转时长