类型检查
Redis中用于操作键(即对象)的命令基本上可以分为两种类型,分别如下
- 可以对任何键进行操作
- 只能对指定键进行操作
比如DEL、EXPIRE、RENAME、TYPE、OBJECT命令,都是可以对任何键,任何Redis对象可以操作的。
//字符串对象
set a 1
//列表对象并添加
rpush b 2 3 4
//集合对象并添加
sadd c 5 6 7
//用del命令删除对象
del a
del b
del c
而另一种命令,只能对限定类型进行操作的,比如各种对象里面规定的命令,如果用在其他命令上会出错,比如GET 列表
类型检查的实现
从上面的报错可以看到,为了确保只有指定类型的键可以执行某些特定的命令,在执行一个命令之前,Redis都会先检查输入键的类型是否正确,然后再决定是否执行命令。
而判断输入键的类型是否是正确,是通过对象RedisObject的type属性去判断来实现的(Type属性标注RedisObject的对象类型,encoding是底层实现)。
整体过程如下
- 在执行一个类型特定命令之前,服务器先检查输入数据库键的值对象是否为执行命令所需的类型(即RedisObject)
- 如果是的话,服务器就对键执行指定的命令
- 如果不是的话,服务器将拒绝执行命令,并且向客户端返回一个类型错误
多态命令的实现
前面已经提过,同一个对象可能有不同的实现底层,比如zset对象有ziplist(压缩列表)和dict与skiplist。所以命令还会有多态实现,就是根据底层的实现不同来执行不同的操作,RedisObject中是使用encoding属性来记录底层实现的,所以总的来说就是根据RedisObject的encoding属性选择正确的实现方式,调用不同。
实际上,DEL、EXPIRE、TYPE等命令其实也算是多态命令。
多态命令的实现大概如下
- 检查键的值对象,即RedisObject的encoding属性是否为对应的属性
- 如果不对应,抛出类型错误异常
- 如果对应,调用对应的函数去执行
内存回收
C语言是不具备内存回收的,需要手动去释放,而Redis是基于C语言设计的,所以Redis在自己的对象系统中构建了自己的内存回收机制。
计数技术
Redis的内存回收机制采用了计数技术实现,通过这一机制,程序可以通过跟踪对象信息的引用计数信息,在适当的时候进行对象释放,并且回收内存
RedisObject对象里面有一个属性名为refcount,其实就是引用计数
typedef RedisObject(
//....前面一系列
....
//引用计数
int refcount;
)RedisObject;
对象的引用计数信息会随着对象的使用状态而不断变化
- 在创建一个新对象的时候,refcount会被设置为1
- 当对象被一个程序所引用的时候,refcount会自增1
- 当对象不再被一个程序引用的时候,refcount会减一
- 当对象的引用计数值变为0时,对象所占用的内存就会被释放掉
总的来说,一个对象正常被引用,然后结束引用后,引用失效,然后refcount减一,知道引用计数值变为0,那么该对象就会被认为是可以回收的对象。
对象共享
计数技术除了可以标记对象是否可以被回收之外,还可以实现内存共享。一般是包含整数值的字符串对象
举个栗子
set a "haha"
set b "haha"
上面两条语句,分别建了a、b两个键,但这两个键的值是一样的,那么就可能有以下情况
- 两个键的对应值对象是不同的
- 连个键的对应值对象是相同的,都是引用同一个对象
很明显,第二种方式会更加的节约内存,那么实现的步骤如下
-
让数据库的键的值指针( * ptr)指向一个现有的值对象,其实就是让b指向的redisObject对象就是a的redisObject对象
-
将被共享的值对象的引用计数增一
生成的结构如下图所示
假如数据库中存在很多个键的值对象是同一个,那么就可以节约很多的内存,不用生成冗余的对象
目前来说,Redis会在初始化服务器时,创建一万个字符串对象,这些对象包含了从0到9999的所有整数值,当服务器需要用到值为0到9999的字符串对象时,服务器就会使用这些共享对象,而不是去新创建对象
共享对象不仅只有字符串键可以使用,其他Redis对象中如果存了字符串对象,那么这些字符串对象也是可以共享的。
不能共享包含字符串的对象(列表,有序集合,集合,哈希)
Redis需要先检查给定的共享对象和键想要创建的目标对象完全相同时,才会将共享对象用作键的值对象,这个过程是要进行比较匹配的,如果共享对象越复杂,那么验证目标对象和共享对象是否相同消耗的资源和需要的时间复杂度就会越高,消耗的CPU时间也会越多
具体消耗的资源如下
- 如果共享对象是保存整数值的字符串对象,验证操作的复杂度为 O ( 1 ) O(1) O(1),只需比较数字是否相同
- 如果共享对象是保存字符串值的字符串对象,验证操作的复杂度为 O ( N ) O(N) O(N),需要将字符串拆开,匹配每一个字符
- 如果共享对象是保存多个字符串值的其他对象,验证操作的复杂度为 O ( N 2 ) O(N^2) O(N2),匹配所有字符串,要匹配N*N个字符
虽然节约了内存,但会受到CPU时间的限制,所以Redis值对包含整数值的字符串对象进行共享
对象的空转时长
除了前面介绍过的type、encoding、prt和refcount四个属性之外,redisObject结构包含的最后一个属性为lru属性,该属性记录了对象最后一次被命令程序访问的时间。
typedef redisObject(
...
//记录对象最后一次被访问的时间
unsigned lru:22;
)redisObject
对象的空转时长是指已经多久没有访问该对象了,只要使用当前时间减去lru属性的值,即可得到对象的空转时长,属性名为lru,那么它就明显是跟LRU算法(最近最少使用)有关,即经常访问的热点数据放LRU列表前面,不常访问的放LRU列表后面
可以看到,当第二次操作msg时,idletime(空转时长)被重置了
空转时长的作用
当Redis打开了maxmemory选项,Redis是采用volatile-lru或者allkeys-lru算法来用于回收内存(其实是标记哪些内存可以回收,计数计数也是一种标记算法),当Redis占用的内存书超过了maxmemory选项所设置的上限值时,空转时长较高的那部分键(即LRU列表里面的OLD区上的数据)会优先被服务器释放,从而回收内存
配置文件里面的maxmemory选项和maxmemory-policy选项就是针对这方面的。