【Redis源码剖析】

来源:互联网 发布:win7仿mac dock栏 编辑:程序博客网 时间:2024/05/18 00:47

在前面六篇文章中,我们逐一分析了字符串sds、双向链表list、字典dict、压缩列表ziplist、压缩字典zipmap、整数集合inset这几种Redis内置数据结构的源码实现(实际上还有一种称作skiplist的结构,这个我们以后遇到再分析),这些内置结构对用户是不可见的(即被隐藏在redis底层,用户感知不到)。接下来我们就要来看看Redis如何在这几种基本内置数据结构的基础上构建自己的数据类型。

Redis中有五种数据类型,分别是:string、hash、list、set、zset。这五种数据类型都是对用户可见的,前面的一些文章中我也介绍了这几种类型相关的操作命令,如果还不熟悉大家可以先温习一下。

今天我们并不讲述上面任何一种数据类型的底层实现,而是先介绍Redis对五种数据类型的封装类(结构体) – redisObject。


1、Redis中的object

为了便于操作,Redis定义了redisObjec结构体来表示string、hash、list、set、zset五种数据类型。redisObject定义在redis.h文件中:

typedef struct redisObject {    unsigned type:4;    unsigned encoding:4;    unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */    int refcount;    void *ptr;} robj;

其中各字段的含义如下:

(1)、4位的type表示具体的数据类型。Redis中共有5中数据类型。2^4 = 8足以表示这些类型。

/* Object types */#define REDIS_STRING 0#define REDIS_LIST 1#define REDIS_SET 2#define REDIS_ZSET 3#define REDIS_HASH 4
(2)、4位的encoding表示该类型的物理编码方式,同一种数据类型可能有不同的编码方式。目前Redis中主要有8种编码方式:

/* Objects encoding. Some kind of objects like Strings and Hashes can be * internally represented in multiple ways. The 'encoding' field of the object * is set to one of this fields for this object. */#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 */

(3)、lru字段表示当内存超限时采用LRU算法清除内存中的对象。
(4)、refcount表示对象的引用计数。
(5)、ptr指针指向真正的存储结构。

那么我们怎么来理解redisObjec结构的作用呢?为了便于操作,Redis采用redisObjec结构来统一五种不同的数据类型,这样所有的数据类型就都可以以相同的形式在函数间传递而不用使用特定的类型结构。同时,为了识别不同的数据类型,redisObjec中定义了type和encoding字段对不同的数据类型加以区别。简单地说,redisObjec就是string、hash、list、set、zset的父类,可以在函数间传递时隐藏具体的类型信息,只是C语言中没有“继承”的概念,所以作者抽象了redisObjec结构来到达同样的目的。

2、Redis数据类型的存储结构

各类型的存储结构如下图所示:

这里写图片描述

具体来说:

  • 字符串string类型可以被编码为 raw (常规字符串) 或者int (用字符串表示64位无符号整数这种编码方式是为了节省空间)。
  • 哈希表Hash类型可以被编码为 ziplist 或者hash_table(就是字典dict)。ziplist 是为了节省列表空间而设计一种特殊编码方式,Redis将两个相邻的节点看作一个键值对从而模拟map结构。
  • 列表list类型可以被编码为ziplist 或者 linkedlist。 ziplist 是为了节省列表空间而设计一种特殊编码方式。
  • 集合set类型被编码为 intset 或者 hash_table。 intset 是为了存储数字的较小集合而设计的一种特殊编码方式。
  • 有序集合被编码为ziplist 或者 skiplist 格式。ziplist可以表示较小的有序集合, skiplist表示任意大小多的有序集合。

    Redis的五种数据类型是以我们前面介绍的内置数据结构为基础实现的,所以如果你对这些数据结构还不了解,建议先看看我前面的文章。

3、redisObject的基本接口

redisObject的相关实现主要涉及redis.h和object.c两个文件,主要包含以下几类接口:

3.1、对象创建

robj *createObject(int type, void *ptr);robj *createStringObject(char *ptr, size_t len);robj *createStringObjectFromLongLong(long long value);robj *createStringObjectFromLongDouble(long double value, int humanfriendly);robj *createListObject(void);robj *createZiplistObject(void);robj *createSetObject(void);robj *createIntsetObject(void);robj *createHashObject(void);robj *createZsetObject(void);robj *createZsetZiplistObject(void);
createXXXObject的方法主要通过调用底层结构的创建方法来创建一个robj对象,例如createZiplistObject方法调用了ziplistNew方法创建一个ziplist然后赋值给robj.ptr指针。其它的就不一一赘述。

/* 创建一个ziplist对象 */robj *createZiplistObject(void) {    unsigned char *zl = ziplistNew();    robj *o = createObject(REDIS_LIST,zl);    o->encoding = REDIS_ENCODING_ZIPLIST;    return o;}

3.2、对象的释放

void freeStringObject(robj *o);void freeListObject(robj *o);void freeSetObject(robj *o);void freeZsetObject(robj *o);void freeHashObject(robj *o);

对象的释放也是通过调用底层结构的方法来释放资源的,不同的是在释放前需要判断该对象的具体编码方式(然后进行相应的释放操作),如freeListObject:

/* 释放一个list对象 */void freeListObject(robj *o) {    // list有两种不同的实现,根据不同的实现释放资源    switch (o->encoding) {    case REDIS_ENCODING_LINKEDLIST:        listRelease((list*) o->ptr);        break;    case REDIS_ENCODING_ZIPLIST:        zfree(o->ptr);        break;    default:        redisPanic("Unknown list encoding type");    }}

3.3、对象的引用计数操作

Redis采用“引用计数法”来管理对象,使用incrRefCount来增加对象的引用计数值,使用decrRefCount来减少对象的引用计数值(如果对象的引用计数值减为0则销毁之)。

void decrRefCount(robj *o);void incrRefCount(robj *o);
3.4、对象操作

对象的操作主要包含下面这些结构,都比较简单,大家可以参看后面的注释代码:

/* 复制一个字符串对象 */robj *dupStringObject(robj *o);/* 检查对象o的类型是否和类型type一致 */int checkType(redisClient *c, robj *o, int type);/* 判断一个对象是否可以用long long型整数表示 */int isObjectRepresentableAsLongLong(robj *o, long long *llongval);/* 尝试对一个字符串进行压缩以节省存储空间,如果无法压缩则增加引用计数后返回 */robj *tryObjectEncoding(robj *o);/* 对一个字符串对象进行解码,如果不能解码则增加其引用计数后返回,否则返回一个新对象 */robj *getDecodedObject(robj *o);/* 获取字符串对象的长度*/size_t stringObjectLen(robj *o);/* 返回指定编码方式的字符串描述 */char *strEncoding(int encoding);/* 比较两个字符串对象是否相等,有两种比较方式:binary(二进制方式)和coll(本地指定的文字排列次序) */int compareStringObjectsWithFlags(robj *a, robj *b, int flags);/* 以REDIS_COMPARE_BINARY的方式比较两字符串对象 */int compareStringObjects(robj *a, robj *b);/* 以REDIS_COMPARE_COLL的方式比较两字符串对象 */int collateStringObjects(robj *a, robj *b);/* 字符串比较 */int equalStringObjects(robj *a, robj *b);/* 获取LRU clock,用于LRU算法 */unsigned long estimateObjectIdleTime(robj *o);/* getLongLongFromObject函数的封装,如果发生错误可以发回指定响应消息 */int getLongFromObjectOrReply(redisClient *c, robj *o, long *target, const char *msg);/* 检查对象o的类型是否和类型type一致 */int checkType(redisClient *c, robj *o, int type);/* getLongLongFromObject函数的封装,如果发生错误可以发回指定响应消息 */int getLongLongFromObjectOrReply(redisClient *c, robj *o, long long *target, const char *msg);/* getLongLongFromObject函数的封装,如果发生错误可以发回指定响应消息 */int getDoubleFromObjectOrReply(redisClient *c, robj *o, double *target, const char *msg);/* 从字符串对象中解析出一个long long型整数 */int getLongLongFromObject(robj *o, long long *target);/* 从字符串对象中解析出一个long double型浮点数 */int getLongDoubleFromObject(robj *o, long double *target);/* getLongDoubleFromObject函数的封装,如果发生错误可以发回指定响应消息 */int getLongDoubleFromObjectOrReply(redisClient *c, robj *o, long double *target, const char *msg);

3.5、object命令

Redis中还有一种命令叫object 命令,这个我们在前面还没有说过,接下来我们简单介绍一下。

object命令允许我们从内存观察给定key的Redis对象。通过object命令可以在内部调试给出keys的内部对象。

object命令支持多个子命令:

(1)、object refcount <key>: 该命令返回指定key对象被引用的次数,主要用于调试。
(2)、object encoding <key>: 该命令返回指定key对象所使用的内部编码方式。
(3)、object idletime <key>: 该命令返回指定key对象自被存储之后空闲的时间,以秒为单位(idle,没有读写操作的请求,即没有被读取也没有被写入) 。

object命令的实现如下:

/* OBJECT命令的实现,命令允许从内部察看给定key的Redis对象。*/void objectCommand(redisClient *c) {    robj *o;    if (!strcasecmp(c->argv[1]->ptr,"refcount") && c->argc == 3) {        if ((o = objectCommandLookupOrReply(c,c->argv[2],shared.nullbulk))                == NULL) return;        addReplyLongLong(c,o->refcount);    } else if (!strcasecmp(c->argv[1]->ptr,"encoding") && c->argc == 3) {        if ((o = objectCommandLookupOrReply(c,c->argv[2],shared.nullbulk))                == NULL) return;        addReplyBulkCString(c,strEncoding(o->encoding));    } else if (!strcasecmp(c->argv[1]->ptr,"idletime") && c->argc == 3) {        if ((o = objectCommandLookupOrReply(c,c->argv[2],shared.nullbulk))                == NULL) return;        addReplyLongLong(c,estimateObjectIdleTime(o));    } else {        addReplyError(c,"Syntax error. Try OBJECT (refcount|encoding|idletime)");    }}