redis设计与实现——对象
前面介绍了那么多数据结构,但redis并不是直接使用它们组成键值对,二是在上面封装了一层创建了一个对象系统。另外,redis的对象系统还实现了基于引用计数的内存回收机制和访问时间记录信息,从而能删除那些空转时长较大的key。
对象的类型与编码
redis使用对象来表示数据库中的key和value,redis的对象实现数据结构在src/server.h中:
1 |
|
类型
object的type字段用于记录对象的类型,分别是字符串、列表、哈希、集合和有序集合。对于redis保存的键值对来说,key总是字符串对象,而value则是上面所说的五种,可用TYPE命令获取对象类型。
1 |
编码和底层实现
对象的ptr指针指向了对象的底层数据结构,但这些数据结构是由对象的encoding属性决定。encoding的取值如下:
1 |
除了OBJ_LIST之外,其它每种类型的对象至少使用了两种不同的编码,使得redis可以根据不同的使用场景来为一个对象设置不同的编码从而优化使用效率。
类型 | 编码 | 对象 |
---|---|---|
OBJ_STRING | OBJ_ENCODING_INT | 用整数值实现的字符串对象 |
OBJ_STRING | OBJ_ENCODING_EMBSTR | 用embstr编码sds的字符串对象 |
OBJ_STRING | OBJ_ENCODING_RAW | 使用sds实现的字符串对象 |
OBJ_LIST | OBJ_ENCODING_QUICKLIST | 使用quicklist实现的列表对象 |
OBJ_HASH | OBJ_ENCODING_ZIPLIST | 使用压缩列表实现的哈希对象 |
OBJ_HASH | OBJ_ENCODING_HT | 使用字典实现的哈希对象 |
OBJ_SET | OBJ_ENCODING_HT | 使用哈希实现的集合对象 |
OBJ_SET | OBJ_ENCODING_INSET | 使用整数集合实现的集合对象 |
OBJ_ZSET | OBJ_ENCODING_ZIPLIST | 使用压缩列表实现的有序集合对象 |
OBJ_ZSET | OBJ_ENCODING_SKIPLIST | 使用跳表实现的有序集合对象 |
字符串对象
由上表可得,字符串的对象编码有三种:int, raw和embst。
当字符串是可以用long类型保存的整数,则转为long。
1 | robj *tryObjectEncoding(robj *o) { |
如果字符串对象保存的字符串值小于或等于44,则用embstr编码的方式,否则用raw编码的方式。之所以选择44个字节,是因为使用了jemalloc,需要将embstr类型的字符串限定在64字节。而redis object占用了16个字节,当字符串长度小于44时sds会采用占用3字节的sdshdr8保存字符串,因此16+3+44=63,再加上字符串末尾的'\0',刚好是64。
1 |
|
其中embstr编码是用来保存短字符串的一种优化的编码方式,虽然其跟raw一样都是采用redisobject结构和sdshdr结构来保存字符串对象,但embstr是调用一次内存分配函数来分配一块连续的空间(raw是调用两次,空间不连续)。
1 | robj *createEmbeddedStringObject(const char *ptr, size_t len) { |
通过将相对于raw两次的内存分配和释放次数降低到一次,并且保存了一块连续的内存空间,也很好地利用了缓存的优势。另外,该编码方式是创建一种unmodifiable string,redis不提供直接修改其的方法。要修改该字符串对象,只能先转为raw。
列表对象
在redis3.2.9之后,quicklist取代了ziplist和linkedlist,成为了列表对象的底层实现。创建一个新的列表对象:
1 | robj *createQuicklistObject(void) { |
由于列表对象只有一种编码方式,因此只是简单调用了quicklistCreate()。
哈希对象
哈希对象有两种编码方式:ziplist或者hashtable。默认是ziplist
1 | robj *createHashObject(void) { |
为了探究其编码转换和插入生成哈希对象的方式,我们先来看hset命令:
1 | void hsetCommand(client *c) { |
首先来看hashTypeLookupWriteOrCreate。
1 | robj *hashTypeLookupWriteOrCreate(client *c, robj *key) { |
接着来看hashTypeTryConversion,通过检查ptr对应sds长度是否比hash_max_ziplist_value更大,则转换到哈希编码的方式
1 |
|
hashTypeSet的作用是往哈希对象添加数据
1 |
|
由此可见,当哈希对象同时满足以下两个条件才会使用ziplist编码:
- 哈希对象保存的所有key/value的字符串长度都小于64个字节;
- 哈希对象保存的键值对个数小于512个;
以上代码都在t_hash.c中。
集合对象
集合对象有两种编码方式:intset或者hashtable
以sadd命令对集合对象的编码方式做解释:
1 | int isSdsRepresentableAsLongLong(sds s, long long *llval) { |
而添加新的值的方式,则是通过调用setTypeAdd()实现的:
1 | int setTypeAdd(robj *subject, sds value) { |
由此可见,当集合对象的个数大于server.set_max_intset_entries(默认为512)或者集合对象保存了非整数值的元素,则需要使用哈希编码。否则可以用整数集合编码。
有序集合对象
有序集合zset有两种编码方式,一种是ziplist,另一种就是skiplist。
我们通过zadd命令来看,这两种编码的使用和转换方法,在src/t_zset.c实现的:
1 | void zaddCommand(client *c) { |
添加或者更新元素的主要实现如下:
1 | int zsetAdd(robj *zobj, double score, sds ele, int *flags, double *newscore) { |
由此可见,在创建skiplist编码的有序集合时,会创建一个zset对象。该zset包含一个字节和一个跳跃表。但两者只会存储一份数据,hashTable和skiplist共享元素的成员和分值。这样就可以保证在执行ZSCORE命令时,通过哈希表可以在O(1)的时间获取结果,而执行ZRANK,ZRANGE这些则可以用skiplist更快得到范围结果。
1 | typedef struct zset { |
类型检查与命令多态
类型检查的实现
为了确保指定类型的键才可以执行某些特定的命令,在执行命令之前会先检查输入键的类型正确与否。
例如当我们使用LLEN命令,其会去检查操作对象是否为一个列表键。
1 | void llenCommand(client *c) { |
有些命令还需要检查对象的编码方式,然后根据不同的编码调用不同的函数。这就是命令多态的来源。
内存回收
C语言不能自动做垃圾回收,因此redis构造了一个引用计数的技术来做内存回收。即redisobject1中refcount字段。
- 当创建新对象时,引用计数为1;
- 当对象被一个新程序使用时,引用计数+1;
- 当对象不再被一个程序使用时,引用计数-1;
- 当对象的引用计数为0,对象所占用的内存被释放;
1 | // 递增引用计数 |
对象共享
为了节省内存,redis会创建一些特殊对象用于全局共享。例如redis会创建10000个字符串对象,包含了从0到9999的所有整数值。那么当服务器要用到这些对象时,会直接取出共享对象。
1 | void trimStringObjectIfNeeded(robj *o) { |
在server.c中预先创建10000个对象。
1 |
|
因此对于这些共享对象,服务器会默认持有一个引用计数。
对象的空转时长
这部分特性通过redisobject的lru属性实现,该字段记录了最后一次被命令程序访问的时间。
使用OBJECT命令可以访问key对象,但不会修改其的lru属性。
1 | void objectCommand(client *c) { |
通过源码可见,这个命令在访问key对象时,不会修改对象的lru属性,因为时直接到db去查找状态的。
而更新lru字段的处理需要经过db.c。
1 | // 底层级的查找 |