Redis数据结构概览(源码分析)

2,524 阅读5分钟

通过学习Redis服务端设计这篇文章,相信大家对redis的整体架构有了最基本的认识,下面来学习Redis的基本结构,大家知道redis性能很高的原因其中之一便是数据结构简单。redis的数据结构支持多种类型,在内存使用方面处理的可谓是“锱铢必较”。那么redis是如何设计的呢?

现在通过服务接收客户端请求及处理流程为例进行分析redis的基础结构 以“set lemon coder”为例:

结构

通过上图,可以看到服务在处理客户端请求的时候 主要为两个步骤 1. 处理输入流(解析请求) 2. 处理命令(将请求存储db,以写命令为例)

下面通过源码来分析 服务端实现的整体流程,本文源码基于Redis版本5.0.5

第一个需要了解的是客户端对象结构,每一个客户端连接,服务端都会创建一个client来与其对应。

src/server.h
/* 服务端创建的客户端对象,只保留了本文需要的字段 */
typedef struct client {
    sds querybuf;           /* 输入缓冲区 */
    int argc;               /* 参数的个数 */
    robj **argv;            /* 具体参数 */
    struct redisCommand *cmd, *lastcmd;  /* 命令处理方法 */
} client;

下面根据这几个字段的处理来分析redis服务如何处理输入流的

解析客户端请求

实现方法为processInlineBuffer()

源码1. 将输入流querybuf转为argv的过程

querybuf为sds类型

argv为robj类型

src/networking.c
processInlineBuffer(){
    /* 使用\r\n分割参数 */
    querylen = newline-(c->querybuf+c->qb_pos);
    aux = sdsnewlen(c->querybuf+c->qb_pos,querylen);
    // 将入参进行拆分为参数
    argv = sdssplitargs(aux,&argc);
    /* 为所有的参数创建redisObject对象 */
    for (c->argc = 0, j = 0; j < argc; j++) {
      if (sdslen(argv[j])) {
          // 将参数进行转换为robj
          c->argv[c->argc] = createObject(OBJ_STRING,argv[j]);
          c->argc++;
       } else {
          sdsfree(argv[j]);
       }
    }
}

源码2. createObject的处理

src/object.c
robj *createObject(int type, void *ptr) {
    robj *o = zmalloc(sizeof(*o));
    o->type = type;
    o->encoding = OBJ_ENCODING_RAW;
    o->ptr = ptr;
    o->refcount = 1;

    /* 设置lru信息*/
    if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
        o->lru = (LFUGetTimeInMinutes()<<8) | LFU_INIT_VAL;
    } else {
        o->lru = LRU_CLOCK();
    }
    return o;
}

处理命令 processCommand()

查找命令对应的处理函数,比如set会寻找到setCommand

对key进行robj+sds类型转换处理

key保存到全局字典dict中

value放入dict中

源码1. 查找命令,执行命令

src/server.c
int processCommand(client *c) {
    /* 当前文件下的 setCommand */
    c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);
    /* 执行命令 */
    call(c,CMD_CALL_FULL);
    return C_OK;
}

源码2. 执行命令setCommand

setCommand为set指令的处理方法,如果指令为hset,那么对应的方法就是hsetCommand

src/t_string.c
/* SET key value [NX] [XX] [EX <seconds>] [PX <milliseconds>] */
void setCommand(client *c) {
  /* 判断过期时间设置等逻辑*/
    .....
  /* 尝试类型encoding */
  c->argv[2] = tryObjectEncoding(c->argv[2]);
  /* 执行基本命令 */
  setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL);
}

源码3. 基本命令的处理

设置key value到字典中

如果有过期属性,设置过期信息

void setGenericCommand(client *c, int flags, robj *key, robj *val, robj *expire, int unit, robj *ok_reply, robj *abort_reply) {
    /* 设置key val 到dict中*/
    setKey(c->db,key,val);
    /* 如果有过期属性,设置过期信息 */
    if (expire) setExpire(c,c->db,key,mstime()+milliseconds);
    addReply(c, ok_reply ? ok_reply : shared.ok);
}

源码4. 存储字典

此处为源码3中setKey的实现,流程为 1. 插入kv(如果key已经存在覆盖设值),2. 移除过期信息,其中插入字典的方法为dbAdd,其中包含sdsup方法即选择合适的结构。

src/db.c
void setKey(redisDb *db, robj *key, robj *val) {
    if (lookupKeyWrite(db,key) == NULL) {
        dbAdd(db,key,val);
    } else {
        dbOverwrite(db,key,val);
    }
    incrRefCount(val);
    removeExpire(db,key);
    signalModifiedKey(db,key);
}
src/db.c
void dbAdd(redisDb *db, robj *key, robj *val) {
    sds copy = sdsdup(key->ptr);
    int retval = dictAdd(db->dict, copy, val);
}

源码5. 类型处理

  1. 将key转化为合适的结构
  2. 将kv插入字典中,此时value为空
  3. 将value转换为合适的结构
  4. 将value插入字典中
src/db.c
void dbAdd(redisDb *db, robj *key, robj *val) {
    sds copy = sdsdup(key->ptr);
    int retval = dictAdd(db->dict, copy, val);

    serverAssertWithInfo(NULL,key,retval == DICT_OK);
    if (val->type == OBJ_LIST ||
        val->type == OBJ_ZSET)
        signalKeyAsReady(db, key);
    if (server.cluster_enabled) slotToKeyAdd(key);
}
src/sds.c
/* 对sds类型进行转换为合适的结构 */
sds sdsdup(const sds s) {
  return sdsnewlen(s, sdslen(s));
}
src/dict.c
/* 添加一个元素到字典中 */
int dictAdd(dict *d, void *key, void *val)
{
    dictEntry *entry = dictAddRaw(d,key,NULL);

if (!entry) return DICT_ERR;
    dictSetVal(d, entry, val);
return DICT_OK;
}
src/dict.h
/* 对val转换为合适的结构 */
define dictSetVal(d, entry, _val_) 
do { 
  if ((d)->type->valDup) 
      (entry)->v.val = (d)->type->valDup((d)->privdata, _val_); 
  else 
      (entry)->v.val = (_val_); 
} while(0)

以上就是服务处理请求的流程,主要包含:处理输入流(解析参数)、存储(确认类型、插入全局字典)。

注意点:

通过上面的对过期时间的设置,会对过期时间进行removeExpire(db,key)操作,操作完成之后会检测是否配置了过期时间,如果没有设置则不过期。看个示例:

127.0.0.1:6379> set lemon coder ex 100
OK
127.0.0.1:6379> pttl lemon
(integer) 95426
127.0.0.1:6379> set lemon coder
OK
127.0.0.1:6379> pttl lemon
(integer) -1

结构编码关系

通过上面的针对字符串的setCommand命令的分析,可以看到查找命令过程,不同的指令会对应不同的处理流程,分别创建不同的类型,并根据值的大小及个数选择合适的数据结构。下面来看下redis有哪些类型和数据结构,以及他们之间的关系。

类型

src/server.h:492
#define OBJ_STRING 0    /* String object. */
#define OBJ_LIST 1      /* List object. */
#define OBJ_SET 2       /* Set object. */
#define OBJ_ZSET 3      /* Sorted set object. */
#define OBJ_HASH 4      /* Hash object. */

数据结构(内部编码)

/* 内部编码*/
#define OBJ_ENCODING_RAW 0     /* Raw 字符串 */
#define OBJ_ENCODING_INT 1     /* int 字符串(全部为数字的情况) */
#define OBJ_ENCODING_HT 2      /* 哈希表 */
#define OBJ_ENCODING_ZIPMAP 3  /* zipmap */
#define OBJ_ENCODING_LINKEDLIST 4 /* linkedlist */
#define OBJ_ENCODING_ZIPLIST 5 /* 压缩列表ziplist */
#define OBJ_ENCODING_INTSET 6  /* intset */
#define OBJ_ENCODING_SKIPLIST 7  /* 跳跃列表skiplist */
#define OBJ_ENCODING_EMBSTR 8  /* 字符串 */
#define OBJ_ENCODING_QUICKLIST 9 /* 快速列表 quickList(linkedList+ziplist) */
#define OBJ_ENCODING_STREAM 10 /* stream */

对应关系

关系

上图为redis的结构及内部编码,后续作者的文章将详细介绍每一种结构,以及每一种结构如何进行编码的转换和扩容及缩容。

以上就是本篇文章的全部内容,感谢您的阅读,如果您在其中发现任何不正确或者不严谨的描述,欢迎指正。

原文地址:Redis数据结构概览(源码分析)

欢迎关注作者公众号: