使用对象的好处:
- 在执行命令之前,根据对象的类型来判断一个对象是否可以执行给定的命令。
- 2.可以针对不同的使用场景,为对象设置多种不同的数据结构实现,从而优化对象在不同场景下的使用效率。
- 3.实现了基于引用计数技术的内存回收机制。
- 4.通过引用计数技术实现了对象共享机制,在适当的条件下,通过让多个数据库键来共享同一个对象来节约内存。
- 5.redis的对象带有访问时间记录信息,可以用于计算数据库键的空转时间。
1. 五种对象
字符串对象,列表对象,哈希对象,集合对象,有序集合对象。
typedef struct redisObject
{
// 类型
unsigned type:4;
// 编码
unsigned encoding:4;
// 指向底层实现数据结构的指针
void *ptr;
} robj;
2 字符串对象(REDIS_STRING)
采用的编码:REDIS_ENCODING_INT, REDIS_ENCODING_EMBSTR, REDIS_ENCODING_RAW
一个字符串对象保存的是整数值,且可以用long类型来表示,那么编码为int。如果保存的是一个字符串值,且长度大于39字节,那么编码为raw。小于等于39字节,编码采用embstr。
2.1 raw和embstr的区别
raw和embstr都是使用redisobject结构和sdshdr结构(即SDS)来表示字符串对象,但raw编码会调用两次内存分配函数来分别创建redisobject和sdshdr,而embstr编码通过调用一次内存分配函数来分配一块连续的空间,空间中依次包含redisobject和sdshdr两个结构。
用embstr编码的字符串对象来保存短字符串值有以下好处:
- embstr编码将创建字符串对象所需的内存分配次数从raw编码的两次降为一次。
- 释放embstr编码的字符串对象只需要调用一次内存释放函数,而释放raw编码的需要两次。
- 因为embstr编码的字符串对象的所有数据都是保存在一块连续的内存里面,所以这种编码的字符串对象比起raw编码的字符串对象能够更好的利用缓存带来的优势。
2.2 编码的转换
int编码的字符串对象,如果对对象执行了一些命令,使得它不再是整数值,那编码就会从int变成raw。
embstr编码的字符串对象是只读的。如果对embstr编码的字符串对象做修改,那程序会先把对象的编码从embstr转换成raw,然后再执行修改命令。所以embstr的字符串对象执行修改命令后,一定会变成一个raw编码的字符串对象。
3 列表对象(REDIS_LIST)
就是普通的list
采用的编码:REDIS_ENCODING_ZIPLIST, REDIS_ENCODING_LINKEDLIST
代表命令:RPUSH,LPUSH,RPOP,LPOP,LLEN
ziplist编码的列表对象使用压缩列表作为底层实现。linkedlist使用双端链表作为底层实现。
3.1 编码转换
- 列表对象保存的所有字符串对象元素的长度都小于64字节;
- 列表对象保存的元素数量小于512个。
当列表对象可以同时满足这上面两个条件时,列表对象使用ziplist编码。否则,使用linkedlist编码。
4 哈希对象(REDIS_HASH)
就是类似于Java的map
采用的编码:REDIS_ENCODING_ZIPLIST, REDIS_ENCODING_HT(hashtable)
当采用了ziplist来保存编码的时候,保存键的节点和保存值的节点一前一后在一起。
代表命令:HSET, HGET, HLEN
4.1 编码转换
- 哈希对象保存的所有键值对的键和值的字符串对象元素的长度都小于64字节;
- 哈希对象保存的元素数量小于512个。
当哈希对象可以同时满足这上面两个条件时,哈希对象使用ziplist编码。否则,使用hashtable编码。
5 集合对象(REDIS_SET)
类似于java的set
采用的编码:REDIS_ENCODING_INTSET, REDIS_ENCODING_HT
代表命令:SADD, SPOP,SCARD
5.1 编码转换
- 集合对象保存的所有元素都是整数值;
- 集合对象保存的元素数量小于512个。
当集合对象可以同时满足这上面两个条件时,集合对象使用intset编码。否则,使用hashtable编码。
6 有序集合对象(REDIS_ZSET)
带有分值(score)的set,在保存上是从小到大,有序的。
采用的编码:REDIS_ENCODING_ZIPLIST, REDIS_ENCODING_SKIPLIST
当采用了ziplist来保存编码的时候,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个保存元素的成员,而第二个元素则保存元素的分值。
代表命令:ZADD, ZCOUNT,ZCARD
6.1 skiplist编码的有序集合对象实现
skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表:
typedef struct zset
{
zskiplist *zsl;
dict *dict
} zset;
跳跃表和字典同时来保存有序集合元素,但这两种数据结构会通过指针来共享相同元素的成员和分值。所以不会产生任何重复成员或者分值,也不会因此浪费内存。
why?
因为使用字典,我们可以以O(1)复杂度查找成员的分值,但字典是无序的。使用跳跃表执行范围型操作的所有优点就会保留下来。所以redis采用两种同时来实现,可以让有序集合的查找和范围型操作都尽可能快的执行。
6.2 编码转换
- 有序集合对象保存的所有元素成员的长度都小于64字节;
- 有序集合对象保存的元素数量小于128个。
当有序集合对象可以同时满足这上面两个条件时,有序集合对象使用ziplist编码。否则,使用skiplist编码。
7. 类型检查与命令多态
redis用于操作键的命令基本可以分为两种类型,一种可以对任何类型的键执行,另一种只能对特定类型的键执行。
7.1 类型检查的实现
类型检查通过redisobject的type属性来实现:
- 在执行一个特定类型的命令之前,服务器会先检查输入数据库键的值对象是否为执行命令所需类型,如果是执行;
- 否则,拒绝执行,并向客户端返回一个类型错误。
7.2 多态命令的实现
redis还会根据值对象的编码方式,选择正确的命令实现代码来执行命令。
举个例子:比如列表对象有ziplist和linkedlist两种编码可用,前者使用压缩列表api来实现列表命令,后者使用双端链表api来实现。这就是多态,只要执行的是某个类型,无论使用哪种编码都可以正常执行。
DEL, EXPIRE等命令也是多态,基于类型的多态,一个命令可以同时处理多种不同类型的键;而LLEN等命令,基于编码的多态,一个命令可以同时处理多种不同编码。
8.内存回收
引用计数技术实现的内存回收机制。对象的整个生命周期可以分为创建对象,操作对象,释放对象三个阶段。
typedef struct redisobject
{
//...
// 引用计数
int refcount;
//...
} robj;
- incrRefCount:将对象的引用计数值+1
- decrRefCount:将对象的引用计数值-1,当对象的引用计数值为0时,释放对象。
- resetRefCount:将对象的引用计数值设置为0,但不释放对象,这个函数通常在需要重新设置对象的引用计数值时使用。
9.对象共享
在redis中,让多个键共享同一个值对象需要执行以下两个步骤:
- 1.将数据库键的指针指向一个现有的值对象;
- 2.将被共享的值对象引用计数+1.
- redis只对包含整数值的字符串对象进行共享(因为需要验证是否相同,耗费cpu时间)。
- 目前在初始化服务器时,会创建一万个字符串对象,包含了从0到9999的所有整数值。
10. 空转时间
typedef struct redisobject
{
//...
// 记录了对象最后一次被命令程序访问的时间
unsigned lru:22;
//...
} robj;
OBJECT IDLETIME 命令可以打印出给定键的空转时长。这个命令在访问键的值对象时,不会修改值对象的lru属性。
键的空转时长还有一个作用:如果服务器打开了maxmemory选项,且回收算法是空转时长,那么久利用这个属性来回收内存,释放空间。