简介
redis 是 key-value 存储系统,其中 key 类型一般为字符串,而 value 类型则为 redis 对象(redis object)。
每次当我们在redis数据库新创建一个值键对时,至少会创建两个对象,一个用于作为建对象,一个用于作为值对象。
redis每个对象都由一个redisObject的结构表示,该结构中与保存数据相关的三个属性分别是:type,encoding,ptr
/*
* Redis 对象
*/
typedef struct redisObject {
// 类型
unsigned type:4;
// 对齐位
unsigned notused:2;
// 编码方式
unsigned encoding:4;
// LRU 时间(相对于 server.lruclock)
unsigned lru:22;
// 引用计数
int refcount;
// 指向对象的值
void *ptr;
}
编码和底层实现
RedisObject中的ptr属性指向对象底层实现的数据结构,encoding属性决定了数据结构的定义。
redis含有以下几种encoding
| 类型 | 编码 | 对象 |
|---|---|---|
| REDIS_STRING | REDIS_ENCODING_INT | 使用整数值实现的字符串对象 |
| REDIS_STRING | REDIS_ENCODING_RAW | 使用简单动态字符串实现的字符串对象 |
| REDIS_STRING | REDIS_ENCODING_EMBSTR | 使用embstr编码的简单动态字符串实现的字符串对象 |
| REDIS_LIST | REDIS_ENCODING_ZIPLIST | 使用压缩列表实现的列表对象 |
| REDIS_LIST | REDIS_ENCODING_LINKENDLIST | 使用双端链表实现的列表对象 |
| 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 | 使用跳跃列表和字典实现的有序集合对象 |
回收方式
redis的对象采用的是引用计数法,进行标记引用使用,当引用为0时,会将该对象销毁。当需要增加或减少引用时,必须调用相应的函数。如果基于redis进行二次开发,程序员必须遵守该准则。
// 增加 Redis 对象引用
void incrRefCount(robj *o) {
o->refcount++;
}
// 减少 Redis 对象引用。特别的,引用为零的时候会销毁对象
void decrRefCount(robj *o) {
if (o->refcount <= 0) redisPanic("decrRefCount against refcount <= 0");
// 如果取消的是最后一个引用,则释放资源
if (o->refcount == 1) {
// 不同数据类型,销毁操作不同
switch(o->type) {
case REDIS_STRING: freeStringObject(o); break;
case REDIS_LIST: freeListObject(o); break;
case REDIS_SET: freeSetObject(o); break;
case REDIS_ZSET: freeZsetObject(o); break;
case REDIS_HASH: freeHashObject(o); break;
default: redisPanic("Unknown object type"); break;
}
zfree(o);
} else {
o->refcount--;
}
}
类型检测实现
对象的属性type,可以用在实现类型检测。
当客户端发送命令时,服务端会根据输入键的值对象的type类型是否为执行命令所需的类型,如果是则对键执行指定的命令。
否则,服务器将拒绝执行指定命令,并返回一个类型错误。
多态命令的实现
redis中存在两种多态命令:
- 类型多态命令:如EXPIRE、DEL、TYPE,不管键的值属于哪种类型,都要保证该执行执行正确
- 编码多态命令:如都是REDIS_LIST的TYPE,但是编码形式有可能是REDIS_ENCOIDNG_ZIPLIST也有可能是REDIS_ENCODING_LINKEDLIST,输入LLEN命令时,针对不同编码,需要选择该编码下的正确的命令实现。
对象共享
对象的属性refcount,除了用于实现引用计数外,还能用来实现整数对象共享的功能。会共享0~9999的字符串对象。
当键A跟键B的值都是一个整数100的对象时,redis不会创建两个新对象,而是创建一个整数100的对象,将refcount设置为2,表示有两个引用,该对象同时被键A和键B共享。数据库中保存相同值的对象越多,对象共享机制就能节约更多的内存。
为什么redis不共享非整数型对象
只有在共享对象跟目标对象完全相同的情况下,程序才会将共享对象作为键的值对象,而一个共享对象保存的数据越复杂,验证共享对象跟目标对象完全相同的过程复杂度就越高,消耗的cpu资源也越多。
- 如果共享对象是保存整数型的字符串对象,验证复杂度为O(1)
- 如果共享对象是保存字符串值的字符串对象,验证复杂度为O(n)
- 如果共享对象时包含多个值的对象,比如哈希对象、列表对象,验证复杂度为O(n^2)
因此,尽管存储更复杂的对象能节约更多的内存,考虑到cpu时间的限制,redis只对保存整数值对象进行共享。
空转时长
RedisObject中属性lru记录了最后一次被程序访问的时间。
OBJECT IDELTIME命令可以打印出给定键的空转时长,其实就是通过当前时间减去键值对象lru记录的时间计算得出。
如果服务器开启了maxmemory选项且内存淘汰策略选择的是allkey-lru或者volatile-lru,当服务器占用的内存超过maxmemory选项所设置的上限时,空转时长较高的那部分键值将会优先被服务器释放,进而回收内存。
参考资料
- Redis设计与实现第三版
- Redis 源码日志 wiki.jikexueyuan.com/project/red…