对象
前面我们介绍了Redis的主要数据结构,如:简单动态字符串SDS、双端链表、字典、压缩列表、整数集合等。Redis并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这个系统包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象,每种对象都用到了至少一种我们之前介绍的数据结构
通过这五种不同类型的对象,Redis可以在执行命令之前,根据对象的类型来判断一个对象是否可以执行给定的命令。使用对象的另一个好处是,我们可以根据不同的使用场景,为对象设置多种不同的数据结构实现,从而优化对象在不同场景下的使用效率
除此之外,Redis的对象系统还实现了基于引用计数的内存回收机制,当程序不再使用某个对象时,这个对象所占用的内存就会被自动释放;另外,Redis还通过引用计数实现了对象共享机制,这一机制可以在适当的条件下,通过让多个数据库键共享一个对象来节约内存
最后,Redis的对象带有访问时间记录信息,该信息可以用于计算数据库键的空转时间,在服务器启用了maxmemory功能的情况下,空转时间较大的那些键可能会优先被服务器删除
对象的类型编码
Redis使用对象来表示数据库中的键和值,每次当我们在Redis的数据库中新创建一个键值对时,我们至少会创建两个对象,一个对象用作键值对的键(键对象),另一个对象用作键值对的值(值 对象)
举个栗子,以下SET命令在数据库将创建一个新的键值对,其中键值对的键是一个包含了字符串"msg"的对象,而键值对的值则是一个包含了字符串"hello world"的对象
127.0.0.1:6379> SET msg "hello world" OK
Redis中的每个对象都由一个redisObject结构表示,该结构中保存数据相关的三个属性分别是:type、encoding、ptr
redis.h
typedef struct redisObject { //类型 unsigned type:4; unsigned notused:2; //编码 unsigned encoding:4; unsigned lru:22; //引用计数 int refcount; //指向底层实现数据结构的指针 void *ptr; } robj;
类型
对象的type属性记录了对象的类型,这个属性的值可以是表1-1列出对的常量中的一个
类型常量 | 对象的名称 |
REDIS_STRING | 字符串对象 |
REDIS_LIST | 列表对象 |
REDIS_HASH | 哈希对象 |
REDIS_SET | 集合对象 |
REDIS_ZSET | 有序集合对象 |
对于Redis数据库保存的键值对来说,键总是一个字符串对象,而值则可以是字符串对象、列表对象、哈希对象、集合对象或者有序集合对象的其中一种,因此:
- 当我们称呼一个数据库键为“字符串键”时,我们指的是“这个数据库键所对应的值为字符串对象”
- 当我们称呼一个数据库键为“列表键”时,我们指的是“这个数据库键所对应的值为列表对象”
TYPE命令的实现方式也与此类似,当我们对一个数据库键执行TYPE命令时,命令返回的结果为数据库键对应的值对象类型,而不是键对象类型:
# 键为字符串对象,值为字符串对象 127.0.0.1:6379> SET msg "hello world" OK 127.0.0.1:6379> TYPE msg string # 键为字符串对象,值为列表对象 127.0.0.1:6379> RPUSH numbers 1 3 5 (integer) 3 127.0.0.1:6379> TYPE numbers list # 键为字符串对象,值为哈希对象 127.0.0.1:6379> HMSET profile name Tome age 25 career Programmer OK 127.0.0.1:6379> TYPE profile hash # 键为字符串对象,值为集合对象 127.0.0.1:6379> SADD fruits apple banana cherry (integer) 3 127.0.0.1:6379> TYPE fruits set # 键为字符串对象,值为有序集合对象 127.0.0.1:6379> ZADD price 8.5 apple 5.0 banana 6.0 cherry (integer) 3 127.0.0.1:6379> TYPE price zset
表1-2列出了TYPE命令在面对不同类型的值对象时所产生的输出
对象 | 对象type属性的值 | TYPE命令的输出 |
字符串对象 | REDIS_STRING | string |
列表对象 | REDIS_LIST | list |
哈希对象 | REDIS_HASH | hash |
集合对象 | REDIS_SET | set |
有序集合对象 | REDIS_ZSET | zset |
编码和底层实现
对象的ptr指针指向对象的底层实现数据结构,而这些数据结构由对象的encoding属性决定。encoding属性记录了对象使用的编码,也即是说这个对象使用了什么数据结构作为对象的底层实现,这个属性的值可以是表1-3列出的常量的其中一个
编码常量 | 编码所对应的底层数据结构 |
REDIS_ENCODING_INT | long类型的整数 |
REDIS_ENCODING_EMBSTR | embstr编码的简单动态字符串 |
REDIS_ENCODING_RAW | 简单动态字符串 |
REDIS_ENCODING_HT | 字典 |
REDIS_ENCODING_LINKEDLIST | 双端链表 |
REDIS_ENCODING_ZIPLIST | 压缩列表 |
REDIS_ENCODING_INTSET | 整数集合 |
REDIS_ENCODING_SKIPLIST | 跳跃表和字典 |
每种类型的对象都至少使用了两种不同的编码,表1-4列出了每种类型的对象可以使用的编码
类型 | 编码 | 对象 |
REDIS_STRING | REDIS_ENCODING_INT | 使用整数值实现的字符串对象 |
REDIS_STRING | REDIS_ENCODING_EMBSTR | 使用embstr编码的简单动态字符串实现的字符串对象 |
REDIS_STRING | REDIS_ENCODING_RAW | 使用简单动态字符串实现的字符串对象 |
REDIS_LIST | REDIS_ENCODING_ZIPLIST | 使用压缩列表实现的列表对象 |
REDIS_LIST | REDIS_ENCODING_LINKEDLIST | 使用双端链表实现的列表对象 |
REDIS_HASH | REDIS_ENCODING_ZIPLIST | 使用压缩列表实现的哈希对象 |
REDIS_HASH | REDIS_ENCODING_HT | 使用字典实现的哈希对象 |
REDIS_SET | REDIS_ENCODING_INTSET | 使用整数集合实现的集合对象 |
REDIS_SET | REDIS_ENCODING_HT | 使用字典实现的集合对象 |
REDIS_ZSET | REDIS_ENCODING_ZIPLIST | 使用压缩列表实现的有序集合对象 |
REDIS_ZSET | REDIS_ENCODING_SKIPLIST | 使用跳跃表和字典实现的有序集合对象 |
使用OBJECT ENCODING命令可以查看一个数据库键的值对象的编码:
127.0.0.1:6379> SET msg "hello wrold" OK 127.0.0.1:6379> OBJECT ENCODING msg "embstr" 127.0.0.1:6379> SADD numbers 1 3 5 (integer) 3 127.0.0.1:6379> OBJECT ENCODING numbers "intset" 127.0.0.1:6379> SADD numbers "seven" (integer) 1 127.0.0.1:6379> OBJECT ENCODING numbers "hashtable"
表1-5列出了不同编码的对象所对应的OBJECT ENCODING命令输出:
对象所使用的底层数据结构 | 编码常量 | OBJECT ENCODING命令输出 |
整数 | REDIS_ENCODING_INT | int |
embstr编码的简单动态字符串(SDS) | REDIS_ENCODING_EMBSTR | embstr |
简单动态字符串 | REDIS_ENCODING_RAW | raw |
字典 | REDIS_ENCODING_HT | hashtable |
双端链表 | REDIS_ENCODING_LINKEDLIST | linkedlist |
双端链表 | REDIS_ENCODING_ZIPLIST | ziplist |
整数集合 | REDIS_ENCODING_INTSET | intset |
跳跃表和字典 | REDIS_ENCODING_SKIPLIST | skiplist |
通过encoding属性来设定对象所使用的编码,而不是为特定类型的对象关联一种固定的编码,极大地提升了Redis的灵活性和效率,因为Redis可以根据不同的使用场景来为一个对象设置不同的编码,从而优化对象在某一场景下的效率。举个栗子,在列表对象包含的元素比较少时,Redis使用压缩列表作为列表对象的底层实现:
- 因为压缩列表比双端链表更节约内存,并且在元素比较少时,在内存中以连续块方式保存的压缩列表比起双端链表可以更快被载入到缓存中
- 随着列表对象包含的元素越来越多,使用压缩列表来保存元素的优势逐渐消失时,对象就会将底层实现从压缩列表转换成功能更强、更适合保存大量元素的双端链表
其他类型的对象也会通过使用多种不同的编码来进行类似的优化,在接下来的内容中,我们将分别介绍Redis中的五种不同类型的对象,说明这些对象底层所使用的编码方式,列出对象从一种编码转换成另一种编码所需的条件,以及同一个命令在多种不同编码上的实现方法
字符串对象
字符串对象的编码可以是int、raw或者embstr。如果一个字符串对象保存的是整数值,并且这个整数值可以用long类型来表示,那么字符串对象会将整数值保存在字符串结构的ptr属性中(将void *转换成long),并将字符串对象的编码设置为int
举个栗子,如果我们执行以下SET命令,那么服务器将创建一个如图1-1所示的int编码的字符串对象作为number键的值:
127.0.0.1:6379> SET number 10086 OK 127.0.0.1:6379> OBJECT ENCODING number "int"
图1-1 int编码的字符串对象
如果字符串对象保存的是一个字符串值,并且这个字符串值的长度大于32字节,那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串值,并将对象的编码设置为raw。举个栗子,如果我们执行以下命令,那么服务器将创建一个如图1-2所示的raw编码的字符串作为store键的值
127.0.0.1:6379> SET story "Long, long, long, long, long ago there lived a king ..." OK 127.0.0.1:6379> STRLEN story (integer) 55 127.0.0.1:6379> OBJECT ENCODING story "raw"
图1-2 raw编码的字符串对象
如果字符串对象保存的是一个字符串,并且这个字符串长度小于等于44字节,那么字符串将使用embstr编码,看下面的示例
127.0.0.1:6379> SET story "Long, long, long, long, long ago there lived" OK 127.0.0.1:6379> STRLEN story (integer) 44 127.0.0.1:6379> OBJECT ENCODING story "embstr" 127.0.0.1:6379> SET story "Long, long, long, long, long ago there lived " OK 127.0.0.1:6379> STRLEN story (integer) 45 127.0.0.1:6379> OBJECT ENCODING story "raw"
embstr编码是专门用于保存短字符串的一种优化编码方式,这种编码方式和raw编码一样,都使用redisObject结构和sdshdr结构来表示字符串对象,但raw编码会调用两次内存分配函数来分别创建redisObject结构和sdshdr结构,而embstr编码则通过调用一次内存分配函数来分配一块连续的空间,空间中依次包含redisObject和sdshdr结构,如图1-3所示
图1-3 embstr编码创建的内存块结构
embstr编码的字符串对象在执行命令时,产生的效果和raw编码的字符串对象执行命令时产生的效果是相同的,但使用embstr编码的字符串来保存短字符串值有以下好处:
- embstr编码将创建字符串对象所需的内存分配次数从raw编码的两次降低为一次
- 释放embstr编码的字符串对象只需调用一次内存释放函数,而释放raw编码的字符串对象需要调用两次内存释放函数
- 因为embstr编码的字符串对象的所有数据都保存在一块连续的内存中,所以这种编码的字符串对象比起raw编码的字符串对象能够更好地利用缓存带来的优势
作为例子,以下命令创建一个embstr编码的字符串对象作为msg键的值,值对象的样子如图1-4所示
127.0.0.1:6379> SET msg "hello world" OK 127.0.0.1:6379> OBJECT ENCODING msg "embstr"
图1-4 embstr编码的字符串对象
最后要说的是,可以用long double类型表示的浮点数在Redis中也是作为字符串值来保存的。如果我们要保存一个浮点数到字符串对象里面,那么程序先将这个浮点数转换成字符串值,然后将其保存。举个栗子,执行以下代码将创建一个包含3.14的字符串对象
127.0.0.1:6379> SET pi 3.14 OK 127.0.0.1:6379> OBJECT ENCODING pi "embstr"
在有需要的时候,程序会将保存在字符串对象中的字符串值转换回浮点数值,执行某些操作,然后再将执行操作所得的浮点数转换回字符串值,并继续保存在字符串对象里面。举个栗子,我们执行以下代码:
127.0.0.1:6379> SET pi 3.14 OK 127.0.0.1:6379> INCRBYFLOAT pi 3.0 "6.14" 127.0.0.1:6379> OBJECT ENCODING pi "embstr
程序首先会取出字符串对象中保存的字符串值"3.14",将它转换回浮点数值3.14,然后把3.14和2.0相加得到5.14后在转换回字符串,并将字符串"5.14"保存到字符串对象中。表1-6总结并列出字符串对象保存各种不同类型的值所使用的编码方式
值 | 编码 |
可以用long类型保存的整数 | int |
可以用long double类型保存的浮点数 | embstr或者raw |
字符串值,或者因为长度太大而没办法用long类型表示的整数,又或者因为长度太大而没办法用long double类型表示的浮点数 | embstr或者raw |
编码的转换
int编码的字符串对象和embstr编码的字符串对象在条件满足的情况下,会被转换为raw编码的字符串对象。对于int编码的字符串对象来说,如果我们向对象执行了一些命令,使得对象保存的不再是整数值,而是一个字符串值,那么字符串对象将从int变为raw
下面的示例中,我们通过APPEND命令,向一个保存整数值的字符串追加一个字符串值,因为追加操作只能对字符串值执行,所以程序会将之前保存的整数值转换为字符串,然后再执行追加操作,操作的执行结果就是一个raw编码的、保存了字符串值的字符串对象
127.0.0.1:6379> SET number 10086 OK 127.0.0.1:6379> OBJECT ENCODING number "int" 127.0.0.1:6379> APPEND number " is a good number!" (integer) 23 127.0.0.1:6379> GET number "10086 is a good number!" 127.0.0.1:6379> OBJECT ENCODING number "raw"
另外,因为Redis没有为embstr编码的字符串对象编写任何相应的修改程序(只有int编码的字符串对象和raw编码的字符串对象有这些程序),所以embstr编码的字符串对象实际上是只读的。当我们对embstr编码的字符串对象执行任何修改命令时,程序先将对象的编码从embstr转换成raw,然后再执行修改命令。因为这个原因,embstr编码的字符串对象在执行修改命令之后,总会变成一个raw编码的字符串对象
以下代码展示了一个embstr编码的字符串对象在执行APPEND命令之后,对象的编码从embstr变为raw的例子:
127.0.0.1:6379> SET msg "hello world" OK 127.0.0.1:6379> OBJECT ENCODING msg "embstr" 127.0.0.1:6379> APPEND msg " again!" (integer) 18 127.0.0.1:6379> OBJECT ENCODING msg "raw"
字符串命令的实现
因为字符串键的值为字符串对象,所以用于字符串键的所有命令都是针对字符串对象来构建的,表1-7例举了其中一部分字符串命令,以及这些命令在不同编码的字符串对象下的实现方法
命令 | int编码的实现方法 | embstr编码的实现方法 | raw编码的实现方法 |
SET | 使用int编码保存值 | 使用embstr编码保存值 | 使用raw编码保存值 |
GET | 拷贝对象所保存的整数值,将这个拷贝转换成字符串值,然后向客户端返回这个字符串值 | 直接向客户端返回字符串值 | 直接向客户端返回字符串值 |
APPEND | 将对象转换成raw编码,然后按raw编码的方式执行此操作 | 将对象转换成raw编码,然后按raw编码的方式执行此操作 | 调用sdscatlen函数,将给定字符串追加到现有字符串的末尾 |
INCRBYFLOAT |
取出整数值并将其转换成long double类型的浮点数,对这个浮点数进行加法计算,然后将得出的浮点数结果保存起来 |
取出字符串值并尝试将其转换成long double类型的浮点数,对这个浮点数进行加法计算,然后将得出的浮点数结果保存起来。 如果字符串值不能被转换成浮点数,那么向客户端返回一个错误 |
取出字符串值并尝试将其转换成long double类型的浮点数,对这个浮点数进行加法计算,然后将得出的浮点数结果保存起来。 如果字符串值不能被转换成浮点数,那么向客户端返回一个错误 |
INCRBY | 对整数值进行加法计算,得出的计算结果会作为整数被保存起来 | embstr编码不能执行此命令,向客户端返回一个错误 | raw编码不能执行此命令,向客户端返回一个错误 |
DECRBY | 对整数值进行减法计算,得出的计算结果会作为整数被保存起来 | embstr编码不能执行此命令,向客户端返回一个错误 | raw编码不能执行此命令,向客户端返回一个错误 |
STRLEN | 拷贝对象所保存的整数值,将这个拷贝转换成字符串值,计算并返回这个字符串值的长度 | 调用sdslen函数,返回字符串的长度 | 调用sdslen函数,返回字符串的长度 |
SETRANGE | 将对象转换成raw编码,然后按raw编码的方式执行此命令 | 将对象转换成raw编码,然后按raw编码的方式执行此命令 | 将字符串特定索引上的值设置为给定的字符 |
GETRANGE | 拷贝对象所保存的整数值,将这个拷贝转换成字符串值,然后取出并返回字符串指定索引上的字符 | 直接取出并返回字符串指定索引上的字符 | 直接取出并返回字符串指定索引上的字符 |