从第七篇开始,基本将重心放在了《redis设计与实现》这本书上,以下是参照书本描述并且对应源码所写,可以算是读书笔记,而不再是阅读源码笔记。
- robj对象数据类型简介
在Redis中底层的数据结构和上层用户接触到的对象类型是分开的,用户所接触到的对象类型可以分为字符串对象(string) 、列表对象(list)、哈希对象(hash)、集合对象(set)以及有序集合对象(sorted set),底层数据结构则有动态字符数组(sds)、双端链表(linked list)、字典(dict)、整数有序集合(intset)、跳跃表(skiplist)、压缩列表(ziplist)等六种数据结构。robj对象数据类型正是一个封装的接口,使得上层用户对象能够在内部封装了底层的不同数据结构。
- robj对象数据类型实现
/* Object types */
#define REDIS_STRING 0
#define REDIS_LIST 1
#define REDIS_SET 2
#define REDIS_ZSET 3
#define REDIS_HASH 4
/* Objects encoding. */
#define REDIS_ENCODING_RAW 0 /* Raw representation */
#define REDIS_ENCODING_INT 1 /* Encoded as integer */
#define REDIS_ENCODING_HT 2 /* Encoded as hash table */
#define REDIS_ENCODING_ZIPMAP 3 /* Encoded as zipmap */
#define REDIS_ENCODING_LINKEDLIST 4 /* Encoded as regular linked list */
#define REDIS_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define REDIS_ENCODING_INTSET 6 /* Encoded as intset */
#define REDIS_ENCODING_SKIPLIST 7 /* Encoded as skiplist */
#define REDIS_ENCODING_EMBSTR 8 /* Embedded sds string encoding */
typedef struct redisObject {
unsigned type:4; //对象类型 字符串 列表 哈希 集合 有序集合
unsigned encoding:4; //编码方式 有8种
unsigned lru:REDIS_LRU_BITS; //字符串最近一次的修改时间
int refcount;
void *ptr;
} robj;
- 用户层对象与底层数据结构(即编码方式)对应关系 
- 字符串对象
string字符串对象的编码方式可以是int、raw或者embstr。
int就是纯数字编码,当字符串保存的是一个纯数字时会常识直接将robj对象中的ptr直接指向一个数字常量;
raw就是所谓的sds动态字符串编码;
embstr是一种更加紧凑的对象。它与sds的区别是:当创建一个对象时,通常会先创建一个robj对象,然后对象中的ptr指针再指向一个由指定字符串创建的sds对象。这里进行了两次内存分配,而当字符串长度小于一定值时,会直接一次性分配robj和sds的空间,直接将所有数据保存在一块连续的内存中,更好的利用缓存带来的优势。
注意点:因为Redis对于embstr编码没有编写相应的修改函数,所以embstr编码的字符串通常为只读模式,想要进行修改就得首先将编码从embstr防止转换成raw再进行。
/*
* 创建一个新 robj 对象
*/
robj *createObject(int type, void *ptr) {
robj *o = zmalloc(sizeof(*o));
o->type = type; //类型指定
o->encoding = REDIS_ENCODING_RAW; //默认编码方式为sds
o->ptr = ptr;
o->refcount = 1;
/* Set the LRU to the current lruclock (minutes resolution). */
o->lru = LRU_CLOCK();
return o;
}
// 创建一个 REDIS_ENCODING_RAW 编码的字符对象
// 对象的指针指向一个 sds 结构
robj *createRawStringObject(char *ptr, size_t len) {
return createObject(REDIS_STRING,sdsnewlen(ptr,len));
}
// 创建一个 REDIS_ENCODING_EMBSTR 编码的字符对象
// 这个字符串对象中的 sds 会和字符串对象的 redisObject 结构一起分配
// 因此这个字符也是不可修改的
robj *createEmbeddedStringObject(char *ptr, size_t len) {
robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr)+len+1); //创建一个obj对象 其实内部空间包括了sdshdr
struct sdshdr *sh = (void*)(o+1); //后移一个obj对象
o->type = REDIS_STRING;
o->encoding = REDIS_ENCODING_EMBSTR;
o->ptr = sh+1; //后移到char*的位置
o->refcount = 1;
o->lru = LRU_CLOCK();
sh->len = len;
sh->free = 0;
if (ptr) {
memcpy(sh->buf,ptr,len); //如果指针存在,将len内容拷贝给sh,否则就初始化全0
sh->buf[len] = '\0';
} else {
memset(sh->buf,0,len+1);
}
return o;
}
//根据len大小确定要创建embstr还是sds
robj *createStringObject(char *ptr, size_t len) {
if (len <= REDIS_ENCODING_EMBSTR_SIZE_LIMIT)
return createEmbeddedStringObject(ptr,len);
else
return createRawStringObject(ptr,len);
}
/*
* 根据传入的整数值,创建一个字符串对象
*
* 这个字符串的对象保存的可以是 INT 编码的 long 值,
* 也可以是 RAW 编码的、被转换成字符串的 long long 值。
*/
robj *createStringObjectFromLongLong(long long value) {
robj *o;
if (value >= 0 && value < REDIS_SHARED_INTEGERS) {
incrRefCount(shared.integers[value]);
o = shared.integers[value]; //指针直接指向一个共享整数
} else {
if (value >= LONG_MIN && value <= LONG_MAX) {
o = createObject(REDIS_STRING, NULL); //创建一个空对象 然后让指针直接指向数值 应该是属于常数区?
o->encoding = REDIS_ENCODING_INT;
o->ptr = (void*)((long)value);
} else {
o = createObject(REDIS_STRING,sdsfromlonglong(value)); //sds构建的long long
}
}
return o;
}
/* Try to encode a string object in order to save space */
// 尝试对字符串对象进行编码,以节约内存。
robj *tryObjectEncoding(robj *o) {
long value;
sds s = o->ptr;
size_t len;
/* Make sure this is a string object, the only type we encode
* in this function. Other types use encoded memory efficient
* representations but are handled by the commands implementing
* the type. */
redisAssertWithInfo(NULL,o,o->type == REDIS_STRING); // 确保是字符串对象
/* We try some specialized encoding only for objects that are
* RAW or EMBSTR encoded, in other words objects that are still
* in represented by an actually array of chars. */
//#define sdsEncodedObject(objptr) (objptr->encoding == REDIS_ENCODING_RAW || objptr->encoding == REDIS_ENCODING_EMBSTR)
if (!sdsEncodedObject(o)) return o;
/* It's not safe to encode shared objects: shared objects can be shared
* everywhere in the "object space" of Redis and may end in places where
* they are not handled. We handle them only as values in the keyspace. */
if (o->refcount > 1) return o; // 不对共享对象进行编码
/* Check if we can represent this string as a long integer.
* Note that we are sure that a string larger than 21 chars is not
* representable as a 32 nor 64 bit integer. */
// 对字符串进行检查
// 只对长度小于或等于 21 字节,并且可以被解释为整数的字符串进行编码 已经改为小于等于20了
len = sdslen(s);
if (len <= 21 && string2l(s,len,&value)) {
/* This object is encodable as a long. Try to use a shared object.
* Note that we avoid using shared integers when maxmemory is used
* because every object needs to have a private LRU field for the LRU
* algorithm to work well.
*这个对象可以编码为long。 尝试使用共享对象。 请注意,我们避免在使用maxmemory时
使用共享整数,因为每个对象都需要有一个私有LRU字段才能使LRU算法正常工作。*/
if ((server.maxmemory == 0 ||
(server.maxmemory_policy != REDIS_MAXMEMORY_VOLATILE_LRU &&
server.maxmemory_policy != REDIS_MAXMEMORY_ALLKEYS_LRU)) &&
value >= 0 &&
value < REDIS_SHARED_INTEGERS)
{
decrRefCount(o);
incrRefCount(shared.integers[value]); //把对象释放直接变成共享对象指针
return shared.integers[value];
} else {
if (o->encoding == REDIS_ENCODING_RAW) sdsfree(o->ptr); //释放sds对象变成常数指针
o->encoding = REDIS_ENCODING_INT;
o->ptr = (void*) value;
return o;
}
}
/* If the string is small and is still RAW encoded,
* try the EMBSTR encoding which is more efficient.
* In this representation the object and the SDS string are allocated
* in the same chunk of memory to save space and cache misses.
*尝试将 RAW 编码的字符串编码为 EMBSTR 编码 */
if (len <= REDIS_ENCODING_EMBSTR_SIZE_LIMIT) {
robj *emb;
if (o->encoding == REDIS_ENCODING_EMBSTR) return o;
emb = createEmbeddedStringObject(s,sdslen(s));
decrRefCount(o);
return emb;
}
/* We can't encode the object...
*
* Do the last try, and at least optimize the SDS string inside
* the string object to require little space, in case there
* is more than 10% of free space at the end of the SDS string.
*
* We do that only for relatively large strings as this branch
* is only entered if the length of the string is greater than
* REDIS_ENCODING_EMBSTR_SIZE_LIMIT. */
// 这个对象没办法进行重编码, 且可用空间大于len/10 尝试从 SDS 中移除所有空余空间
if (o->encoding == REDIS_ENCODING_RAW &&
sdsavail(s) > len/10)
{
o->ptr = sdsRemoveFreeSpace(o->ptr);
}
/* Return the original object. */
return o;
}
- 列表对象
Redis中list列表对象底层编码方式可以由ziplist或者linkedlist两种方式实现,自然而然的地,通常当列表的数据量较小时就会使用ziplist方式构建robj对象,而当数据量较大时则会直接由linkdlist进行构建,在操作过程中导致数据量增加也会促使编码方式由ziplist想linklist转换。
当列表对象同时满足一下两个条件时,才会使用ziplist编码:
1.保存的字符串元素长度都小于64字节
2.保存的元素数量小于512个;
任一条件不满足就会转换为linkedlist编码方式。
(ps:1:这里所说的字符串元素可以是sds,因为数据结构之间通常可以相互嵌套,当然一般套的都是字符串;2.以上条件的上限值是可以通过配置文件进行修改的)
- 哈希对象
Redis中哈希对象底层编码可以由ziplist或者dict实现。因为哈希对象通常是键值对,因此在ziplist中键和值是作为相邻的两个节点同时保存进去的。通常每次键值对的添加是从表尾进行添加。
当列表对象同时满足一下两个条件时,才会使用ziplist编码:
1.保存的键值对中键长和值长度都小于64字节
2.保存的元素数量小于512个
任一条件不满足就会转换为dict编码方式。
- 集合对象
Redis中集合对象底层编码可以由intset或者dict实现。当存入的元素均是整数时会使用intset进行存储,一旦出现非整数元素,编码方式就会转换为dict实现。而针对字典编码方式来说,通常字典有键和值两个方面,在存储集合对象时,通常是将集合对象作为键传入进行字典的创建,而将字典的值设置为null。
当集合对象同时满足一下两个条件时,对象使用intset编码:
1.集合对象保存的所有元素都是整数值
2.集合对象保存的元素数量不超过512个
- 有序集合对象 Redis中有序集合对象底层编码可以由ziplist或者skiplist实现。因为ziplist存储原本是无序的,所以当要使用这一编码来创建有序集合对象时,每次插入新数据都会按照分值大小进行一个排序,而不是单纯的从表尾插入。
针对skiplist编码方式还添加了一个中间层zset结构体,该结构体包含了一个skiplist和一个dict,来共同完成有序集合对象的功能。之所以使用两个底层数据结构是为了将两种数据结构的优点结合缺点互补。skiplist数据结构是有序的,可以完成范围查找;而dict字典结构虽然无序但是从分值查找其对应数据的时间复杂度为O(1)。另外由于dict和skiplist可以共用键值对数据,所以内存上的消耗只有指针的分配,相对于键值对的实际内容来说可以忽略不计。
- 类型检查以及命令多态的实现
那么当用户输入一个命令时,Redis是如何知道输入命令所针对的对象是怎样的类型,应该调用什么函数来实现操作的呢?这里就要依靠类型检查,类型检查分为两步,对应robj对象中的type和encoding值,会首先检查type来确保要执行的命令是否是该对应类型可以执行的操作,这就是类型检查;当发现是可执行的操作之后,再对encoding进行检查,当知道了encoding具体是什么,那么就可以调用具体编码方式的具体api来完成相应操作,这就是命令多态。
- 内存回收
Redis在自己对象系统中构建了自己的内存回收机制,因为就C语言本身来说是不具备自动回收功能的,通常用户在申请空间存储数据之后还需要自己手动释放空间,Redis则通过构建了一个引用计数技术来实现了自动的内存回收机制。通过这一机制,程序会跟踪对象的引用计数信息,在适当的时候自动释放对象并进行内存回收。
- 对象共享
Reids在节约内存上面做了很多努力,其中结合引用计数设计的共享对象机制就是一种节约内存的方法,当出现两个同样的对象时通常会只修改实际对象的引用计数而并不进行实际的对象复制,只有在有需要时才进行复制(写时复制),比如对对象进行了修改。
对于整数数据来说,Redis在初始化服务器时创建了包含0-9999这一万个字符串对象,当然编码方式肯定是INT,当需要创建这些整数数据时都会直接使用这些共享对象而不会自己创建新的对象。(ps.这个共享字符串的数量也是可以通过配置修改的。)
- 对象的空转时长
空转时长就是指某个对象,假设其最后一次被访问世间为t1 ,当前世间为t0,那么t0-t1即为空转时长。
Redis的对象数据结构robj中,前面实现中可以看就有一个url数据成员,这个成员代表的就是该对象最后一次被命令程序访问的时间(在查询对象的类型和编码方式时不会改变这一成员变量),如果服务器打开了maxmemory功能,并且服务器用于回收内存的算法为volatile-lru或者allkeys-lru时,那么当服务器占用 的内存数超过了限制值时会将空转时长较高的那部分对象优先被服务器释放,从而回收内存。
- 重点回顾
- Redis数据库中每个键值对的键和值都是一个对象
- Redis共有五种类型对象,每种类型对象至少都有两种或者以上的编码实现方式,不同的编码可以在不同的使用场景上优化对象的使用效率。
- 服务器在执行某些命令前会先检查给定键的类型能否执行指定的命令,而检查一个键的类型就是检查键的值对象类型
- Redis的对象系统带有引用计数实现的内存回收机制,当一个对象不再被使用时,该对象所占用的内存就会被自动释放。
- Redis会共享值为0到9999的字符串对象
- 对象会记录最后一次被访问时间,可以用于计算对象的空转时间;