Redis基于SDS、双端链表、字典、压缩列表、整数集合等数据结构创建了一个对象系统,这个系统包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象,每种对象都用到了至少一种上述数据结构。通过这五种不同类型的对象,Redis可以在执行命令之前,根据对象的类型来判断一个对象是否可以执行给定的命令。使用对象的另一个好处是,可以针对不同的使用场景,为对象设置多种不同的数据结构实现,从而优化对象在不同场景下的使用效率。

1.对象的结构

Redis使用对象来表示数据库中的键和值,每当新创建一个键值对时,至少会创建两个对象:键对象,总是一个字符串对象;值对象,可以是字符串对象、列表对象、哈希对象、集合对象、有序集合对象中的一种。Redis对象都由一个redisObject结构表示,结构如下:

typedef struct redisObject{    
    // 类型
    unsigned type:4;    
    // 编码    
    unsigned encoding:4;    
    // 指向底层实现数据结构的指针    
    void *ptr;    
    // 引用计数    
    int refcount;    
    // 对象的空转时长    
    unsigned lru:22; 
}robj;

(1)类型type

对象的type属性记录了对象的类型,对一个数据库键执行TYPE命令时,命令返回的结果为数据库键对应的值对象的类型。type的属性值与TYPE命令的输出结果对应如下:

redis 存储对象类型命令 redis存储对象集合_Redis

(2)编码encoding

encoding属性记录了对象使用了什么数据结构作为对象的底层实现,使用OBJECT ENCODING命令可以查看一个数据库键的值对象的编码。通过encoding属性来设定对象所使用的编码,而不是为特定类型的对象关联一种固定的编码,极大地提升了Redis的灵活性和效率,Redis可以根据不同的使用场景来为个对象设置不同的编码,从而优化对象在某一场景下的效率。

同一种类型在不同的场景下,有多种不同编码实现,对应如下:

redis 存储对象类型命令 redis存储对象集合_字符串_02

(3)指针*ptr

指针*ptr指向值对象的指针。

(4)引用计数器refcount

refcount记录对象被引用的次数。

(5)空转时长lru

lru属性记录了对象最后一次被命令程序访问的时间,OBJECT IDLETIME命令可以打印出给定键的空转时长,这一空转时长就是通过将当前时间减去键的值对象的lru时间计算得出的。当访问某个键值对的时候,lru会被修改为最近一次被访问的时间。OBJECT IDLETIME命令的实现是特殊的,这个命令在访问键的值对象时,不会修改值对象的lru属性。

键的空转时长的作用:如果服务器打开了maxmemory选项,并且服务器用于回收内存的算法为volatile-lru或者allkeys-lru,那么当服务器占用的内存数超过了maxmemory选项所设置的上限值时,空转时长较高的那部分键会优先被服务器释放,从而回收内存。

2.类型检查与命令多态

(1)命令的类型

Redis中用于操作键的命令基本上可以分为两种类型:

一种命令可以对任何类型的键执行,比如说DEL命令、EXPIRE命令、RENAME命令、TYPE命令、OBJECT命令等。

另一种命令只能对特定类型的键执行,比如说:

  • SET、GET、APPEND、STRLEN等命令只能对字符串键执行;
  • HDEL、HSET、HGET、HLEN等命令只能对哈希键执行;
  • RPUSH、LPOP、LINSERT、LEN等命令只能对列表键执行;
  • SADD、SPOP、SINTER、SCARD等命令只能对集合键执行;
  • ZADD、ZCARD、ZRANK、ZSCORE等命令只能对有序集合键执行

(2)类型检查的实现

为了确保只有指定类型的键可以执行某些特定的命令,在执行一个类型特定的命令之前,Redis会先检查输入键的类型是否正确,然后再决定是否执行给定的命令。类型特定命令所进行的类型检查是通过redisObject结构的type属性来实现的,在执行一个类型特定命令之前,服务器会先检查目标值对象是否为执行命令所需的类型,如果是的话,服务器就对键执行指定的命令;否则,服务器将拒绝执行命令,并向客户端返回一个类型错误。

Redis除了会根据值对象的类型来判断键是否能够执行指定命令之外,还会根据值对象的编码方式,选择正确的命令实现代码来执行命令。例如,列表对象有ziplist和linkedlist两种编码可用,其中前者使用压缩列表API来实现列表命令,而后者则使用双端链表API来实现列表命令。在对一个键执行LLEN命令,那么服务器除了要确保目标值对象是列表对象外,还需要根据值对象所使用的编码来选择正确的LLEN命令:

  • 如果列表对象的编码为ziplist,程序将使用ziplistLen函数来返回列表的长度;
  • 如果列表对象的编码为llinkedlist,程序将使用listLength函数来返回双端链表的长度;

可以将DEL、EXPIRE、TYPE等命令也称为多态命令,因为无论输入的键是什么类型,这些命令都可以正确地执行。DEL、EXPIRE等命令和LLEN等命令的区别在于,前者是基于类型的多态个命令可以同时用于处理多种不同类型的键,而后者是基于编码的多态—个命令可以同时用于处理多种不同编码。

3.引用计数的作用

(1)内存回收

C语言并不具备自动内存回收功能,Redis在自己的对象系统中构建了一个引用计数(reference counting)技术实现的内存回收机制,通过跟踪对象的引用计数信息,在适当的时候自动释放对象并进行内存回收,每个对象的引用计数信息由redisObject结构的 refcount属性记录。对象的引用计数信息会随着对象的使用状态而不断变化:

  • 创建一个新对象时,refcount会被初始化为1;
  • 对象被一个新程序使用时,refcount会被加一;
  • 对象不再被一个程序使用时,refcount会被减一;
  • **refcount==**0时,对象所占用的内存会被释放。

refcount相关API如下:

redis 存储对象类型命令 redis存储对象集合_值对象_03

(2)对象共享

引用计数除了应用于内存回收之外,还能实现对象共享的作用,共享对象机制对于节约内存非常有帮助,数据库中保存的相同值对象越多,对象共享机制就能节约越多的内存。在Redis中,让多个键共享同一个值对象需要执行以下两个步骤:

  1. 将数据库键的值指针指向一个现有的值对象;
  2. 将被共享的值对象的引用计数增一。

Redis会在初始化服务器时,创建字符串对象,这些对象包含了从0到9999的所有整数值,当服务器需要用到值为0到9999的字符串对象时,服务器就会使用这些共享对象,而不是新创建对象。创建共享字符串对象的数量可以通过修改redis.h/REDIS SHARED INTEGERS常量来修改。

举个例子,如果我们创建一个值为100的键A,并使用OBJECT REFCOUNT命令查看键A的值对象的引用计数,我们会发现值对象的引用计数为2,引用这个值对象的两个程序分别是持有这个值对象的服务器程序,以及共享这个值对象的键A。

另外,这些共享对象不单单只有字符串键可以使用,那些在数据结构中嵌套了字符串对象的对象(linkedlist编码的列表对象、hashtable编码的哈希对象、hashtable编码的集合对象,以及zset编码的有序集合对象)都可以使用这些共享对象。

服务器在考虑是否设置为共享对象时,程序需要先检查共享对象和键的目标对象是否完全相同,只有在共享对象和目标对象完全相同的情况下,程序才会将共享对象用作键的值对象。共享对象的值越复杂,验证共享对象和目标对象是否相同所需的复杂度就会越高,消耗的CPU时间也会越多:

  • 如果共享对象是保存整数值的字符串对象,那么验证操作的复杂度为O(1);
  • 如果共享对象是保存字符串值的字符串对象,那么验证操作的复杂度为O(N);
  • 如果共享对象是包含了多个值(或者对象的)对象,比如列表对象或者哈希对象,那么验证操作的复杂度将会是O(N^2)。

因此,尽管共享更复杂的对象可以节约更多的内存,但受到CPU时间的限制,Redis只对包含整数值的字符串对象进行共享

4.容器型数据结构的通用规则

list/set/hash/zset这四种数据结构是容器型数据结构,它们共享下面两条通用规则:

(1)create if not exists

如果容器不存在,那就创建一个,再进行操作。比如,rpush操作刚开始是没有列表的,Redis就会自动创建一个,然后再 rpush 进去新元素。

(2)drop if no elements

如果容器里元素没有了,那么立即删除容器,释放内存。这意味着lpop 操作到最后一个元素,列表就消失了。

(3)过期时间

Redis所有的数据结构都可以设置过期时间,时间到了,Redis会自动删除相应的对象。需要注意的是过期是以对象为单位,比如一个hash结构的过期是整个hash对象的过期,而不是其中的某个子key。如果一个字符串已经设置了过期时间,然后调用了set方法修改了它,它的过期时间会消失。Redis的ttl命令以秒为单位返回key的剩余过期时间。

redis 存储对象类型命令 redis存储对象集合_值对象_04