19、Redis 源码解析 - Redis 数据类型源代码,学习总结

经过将近半个月的学习,终于将五种数据类型的源代码都学习了一遍,虽然不是全部阅读,但是大部分的代码都已经学习到了,趁五一假期好好整理和总结一下近期我们学习的内容。

1 数据类型介绍

在Redis中有五种数据类型,分别是字符串、列表、集合、有序集合、哈希,在源代码 redis.h 头文件中,有对应他们的常量定义,每次判断对象类型的时候会使用到这几个常量。

/* Object types */
#define REDIS_STRING 0
#define REDIS_LIST 1
#define REDIS_SET 2
#define REDIS_ZSET 3
#define REDIS_HASH 4

Redis中,对应五种的数据类型的源代码有五个文件,分别是:

  • t_string.c => 字符串
  • t_list.c => 列表
  • t_set.c => 集合
  • t_zset.c => 有序集合
  • t_hash.c => 哈希

2 redisObject

2.1 代码回顾

这几天我们学习源代码的时候能经常碰到 redisObject 这个东西,在代码中常常是使用 robj 这个名称来表示一个redisObject,下面选一些代码片段来回忆一下。

robj *expire = NULL;

robj *next = (j == c->argc-1) ? NULL : c->argv[j+1];

robj *o;

robj *o, *new, *aux;

robj *key = lookupKeyWrite(c->db, c->argv[1]);

robj *value = listTypePop(o,where);

2.2 结构体定义

在代码里随便一搜就能找到很多使用 robj 的地方,说明redisObject使用的非常多,接下来看下他的结构体定义都有些啥。

/* A redis object, that is a type able to hold a string / list / set */
//一个redis object,可以保存字符串、列表、集合等数据。

/* The actual Redis Object */
#define REDIS_LRU_BITS 24
#define REDIS_LRU_CLOCK_MAX ((1<<REDIS_LRU_BITS)-1) /* Max value of obj->lru */
#define REDIS_LRU_CLOCK_RESOLUTION 1000 /* LRU clock resolution in ms */
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;

可以发现 redisObject 的属性并不多,分别是:

  • type :数据类型
  • encoding:数据编码
  • lru:最近访问时间
  • refcount:对象引用数量
  • *ptr:值的指针

这五个属性除了lru,我们还没有遇到过,其他几个属性在以往我们学习的代码中,其实有遇到过很多次,下面找几段代码来回忆。

2.3 type 属性实例

   
   robj *o;
   
    if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk)) == NULL)
        return REDIS_OK;

    if (o->type != REDIS_STRING) {
   
     
        addReply(c,shared.wrongtypeerr);
        return REDIS_ERR;
    } 
    

这一段代码先是获取一个 redisObject,然后通过访问这个结构体的 type 属性,用来判断对象类型是否为字符串,现在我们了解了 redisObject 的内部构造之后,再来看这一段代码就十分清楚,他在做什么。

redisObject 的 type属性是用来保存一个redis对象的数据类型,常量值就是上面我们说过的那几个值。

2.3 encoding 属性实例


void listTypeTryConversion(robj *subject, robj *value) {
   
     
    if (subject->encoding != REDIS_ENCODING_ZIPLIST) return;
    if (sdsEncodedObject(value) &&
        sdslen(value->ptr) > server.list_max_ziplist_value)
            listTypeConvert(subject,REDIS_ENCODING_LINKEDLIST);
}

这一段代码通过传入的 robj类型的 subject,获取这个结构体的 encoding 属性,来判断他的对象编码是否压缩表,因为一个数据类型可能会使用不同的数据编码来实现,比如列表可以使用压缩表或者链表,对象编码常量也定义在redis.h中。


/* 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 */
#define REDIS_ENCODING_EMBSTR 8  /* Embedded sds string encoding */

2.4 refcount 属性实例

	//t_string.c
    o = lookupKeyWrite(c->db,c->argv[1]);
    if (o == NULL) {
   
     
        /* Create the key */
        c->argv[2] = tryObjectEncoding(c->argv[2]);
        dbAdd(c->db,c->argv[1],c->argv[2]);
        incrRefCount(c->argv[2]);
        totlen = stringObjectLen(c->argv[2]);
	}
	
	//object.c
	void incrRefCount(robj *o) {
   
     
    	o->refcount++;
    }
}

这一段代码使用 incrRefCount 增加 引用值,这个方法里的代码很简单就直接对 这个结构体的 refcount 属性进行自增。

2.5 ptr 实例

	//t_string.c
	sds value = c->argv[3]->ptr;
	
	//t_list.c
	ln = listIndex(o->ptr,start);
	
	//t_hash.c
	zl = o->ptr;
	

从不同几个源代码文件里找了几个片段,可以看到不同数据类型的ptr指针代表的是不同的东西,是指向他们真正的数据的指针。

3 数据结构

3.1 对应关系

学习完五种数据类型的代码我们可以发现,他们的编码属性可以是多种可能,下面看下他们都对应着哪些编码类型。

type encoding
REDIS_STRING REDIS_ENCODING_RAW 、REDIS_ENCODING_EMBSTR 、REDIS_ENCODING_INT
REDIS_LIST REDIS_ENCODING_ZIPLIST 、REDIS_ENCODING_LINKEDLIST
REDIS_HASH REDIS_ENCODING_ZIPLIST 、REDIS_ENCODING_HT
REDIS_SET REDIS_ENCODING_INTSET 、REDIS_ENCODING_HT
REDIS_ZSET REDIS_ENCODING_ZIPLIST 、REDIS_ENCODING_SKIPLIST

type 与 encoding之间的关系,肯能一时看过去比较复杂,但其实也没有必要尝试去硬记住他们,因为只有理解了他们才能更好的知道为什么是这样,而不是单纯地记他们是这样的。

3.2 encoding 转换

一个数据类型如果使用了多种数据类型,那一般都会有一个默认的数据类型,并且在新增的时候一般都会判断是否要转换数据类型。

void listTypeTryConversion(robj *subject, robj *value) {
   
     
    if (subject->encoding != REDIS_ENCODING_ZIPLIST) return;
    if (sdsEncodedObject(value) &&
        sdslen(value->ptr) > server.list_max_ziplist_value)
            listTypeConvert(subject,REDIS_ENCODING_LINKEDLIST);
}

比如上面一个代码片段,如果值的长度超过压缩表的相关配置,那么就会进行数据结构的转换,这些配置定义在redis.h中。

#define REDIS_HASH_MAX_ZIPLIST_ENTRIES 512
#define REDIS_HASH_MAX_ZIPLIST_VALUE 64
#define REDIS_LIST_MAX_ZIPLIST_ENTRIES 512
#define REDIS_LIST_MAX_ZIPLIST_VALUE 64
#define REDIS_SET_MAX_INTSET_ENTRIES 512
#define REDIS_ZSET_MAX_ZIPLIST_ENTRIES 128
#define REDIS_ZSET_MAX_ZIPLIST_VALUE 64

4 方法与命名

很多代码中的方法命名都有规律,通过了解这些规律我们能大概知道这些方法是干什么的,增加阅读代码的速度。

4.1 包含 Command的方法

方法名出现Command的方法,代表这是一个命令方法,并且Commnd前面的部分就是命令本身,比如:

	void setCommand(redisClient *c)
	
	void setnxCommand(redisClient *c)
	
	void setexCommand(redisClient *c)

	void lpushCommand(redisClient *c)
	
	void rpushCommand(redisClient *c)

4.2 包含 Generic 的方法

方法名出现Generic的方法,代表这是一个通用方法,会被多个方法调用,比如:

void setnxCommand(redisClient *c) {
   
     
    c->argv[2] = tryObjectEncoding(c->argv[2]);
    setGenericCommand(c,REDIS_SET_NX,c->argv[1],c->argv[2],NULL,0,shared.cone,shared.czero);
}

void setexCommand(redisClient *c) {
   
     
    c->argv[3] = tryObjectEncoding(c->argv[3]);
    setGenericCommand(c,REDIS_SET_NO_FLAGS,c->argv[1],c->argv[3],c->argv[2],UNIT_SECONDS,NULL,NULL);
}

void psetexCommand(redisClient *c) {
   
     
    c->argv[3] = tryObjectEncoding(c->argv[3]);
    setGenericCommand(c,REDIS_SET_NO_FLAGS,c->argv[1],c->argv[3],c->argv[2],UNIT_MILLISECONDS,NULL,NULL);
}

上面代码中 setnxCommand、setexCommand、psetexCommand 都调用了 setGenericCommand 这个方法。

void lpushCommand(redisClient *c) {
   
     
    pushGenericCommand(c,REDIS_HEAD);
}

void rpushCommand(redisClient *c) {
   
     
    pushGenericCommand(c,REDIS_TAIL);
}

上面代码中 lpushCommand、rpushCommand 都调用了 pushGenericCommand 这个方法。

4.3 包含 Type 的方法

void listTypeInsert(listTypeEntry *entry, robj *value, int where)

robj *listTypeGet(listTypeEntry *entry)

int listTypeNext(listTypeIterator *li, listTypeEntry *entry)
void hashTypeTryObjectEncoding(robj *subject, robj **o1, robj **o2)

void hashTypeTryConversion(robj *o, robj **argv, int start, int end)

unsigned long setTypeSize(robj *subject)

int setTypeIsMember(robj *subject, robj *value)

除了t_string.c,其他的几个源代码中,都很明显的把方法分成两大类,一类是API,一类是Command,这种一般带Type的方法都是属于API一类的,带Command的是属于Command一类的,而且一般都是Command方法来调用这些Type的方法。

4.4 包含 addReply 的方法

方法名出现 addReply 的方法,代表这是响应客户端的方法,比如:

 	addReply(c, abort_reply ? abort_reply : shared.nullbulk);

	addReplyLongLong(c,olen);
	
	addReplyBulk(c,o);
	
	addReplyError(c,"offset is out of range");

	addReplyErrorFormat(c,"invalid expire time in %s",c->cmd->name);

	addReplyBulkCBuffer(c,(char*)str+start,end-start+1);

	addReplyMultiBulkLen(c,c->argc-1);

	...

在redis代码中,出现大量这种包含 addReply 的方法,虽然他们都是响应客户端,但是有很多变种的方法,他们定义在networking.c文件中,后续等我们学习到了再详细研究,这里暂且知道他们是用来响应客户端的就可以,就是我们平常输入命令得到的结果。

4.5 包含 lookupKey 的方法

方法名出现 lookupKey 的方法,代表查询某个键的redis对象的方法,比如:

o = lookupKeyWrite(c->db,c->argv[1]);

robj *o = lookupKeyRead(c->db,c->argv[j]);

o = lookupKeyReadOrReply(c,c->argv[1],shared.emptybulk)

在redis代码中,出现大量这种包含 lookupKey 的方法,通过名称大概能知道是查询键的意思,他们集中定义在 db.c 文件当中,后续我们回详细研究,目前先暂且知道他们是根据某个键值查询redis对象的就可以,通常传入的参数都是客户端或者db和键值。

5 redisClient

在很多Command方法的参数中,我们能够看到 redisClient 这个结构体的熟客,现在就稍微简单的了解一下它,要不然总是会感到很陌生,它的结构体定义在 redis.h 中。

5.1 结构体定义

/* With multiplexing we need to take per-client state.
 * Clients are taken in a linked list. */
typedef struct redisClient {
   
     
    uint64_t id;            /* Client incremental unique ID. */
    int fd;
    redisDb *db;
    int dictid;
    robj *name;             /* As set by CLIENT SETNAME */
    sds querybuf;
    size_t querybuf_peak;   /* Recent (100ms or more) peak of querybuf size */
    int argc;
    robj **argv;
    struct redisCommand *cmd, *lastcmd;
    int reqtype;
    int multibulklen;       /* number of multi bulk arguments left to read */
    long bulklen;           /* length of bulk argument in multi bulk request */
    list *reply;
    unsigned long reply_bytes; /* Tot bytes of objects in reply list */
    int sentlen;            /* Amount of bytes already sent in the current
                               buffer or object being sent. */
    time_t ctime;           /* Client creation time */
    time_t lastinteraction; /* time of the last interaction, used for timeout */
    time_t obuf_soft_limit_reached_time;
    int flags;              /* REDIS_SLAVE | REDIS_MONITOR | REDIS_MULTI ... */
    int authenticated;      /* when requirepass is non-NULL */
    int replstate;          /* replication state if this is a slave */
    int repl_put_online_on_ack; /* Install slave write handler on ACK. */
    int repldbfd;           /* replication DB file descriptor */
    off_t repldboff;        /* replication DB file offset */
    off_t repldbsize;       /* replication DB file size */
    sds replpreamble;       /* replication DB preamble. */
    long long reploff;      /* replication offset if this is our master */
    long long repl_ack_off; /* replication ack offset, if this is a slave */
    long long repl_ack_time;/* replication ack time, if this is a slave */
    long long psync_initial_offset; /* FULLRESYNC reply offset other slaves
                                       copying this slave output buffer
                                       should use. */
    char replrunid[REDIS_RUN_ID_SIZE+1]; /* master run id if this is a master */
    int slave_listening_port; /* As configured with: SLAVECONF listening-port */
    int slave_capa;         /* Slave capabilities: SLAVE_CAPA_* bitwise OR. */
    multiState mstate;      /* MULTI/EXEC state */
    int btype;              /* Type of blocking op if REDIS_BLOCKED. */
    blockingState bpop;     /* blocking state */
    long long woff;         /* Last write global replication offset. */
    list *watched_keys;     /* Keys WATCHED for MULTI/EXEC CAS */
    dict *pubsub_channels;  /* channels a client is interested in (SUBSCRIBE) */
    list *pubsub_patterns;  /* patterns a client is interested in (SUBSCRIBE) */
    sds peerid;             /* Cached peer ID. */

    /* Response buffer */
    int bufpos;
    char buf[REDIS_REPLY_CHUNK_BYTES];
} redisClient;

乍一看,我的天怎么这么多属性,感到害怕,不要惊慌也不要害怕,我们这里先简单了解下两个我们经常碰到的属性就可以,它们分别是argv、argc。

argv 用来保存客户端输入的所有参数,包括命令本身。

argc 用来保存参数的数量。

5.2 代码实例

下面来看一些代码实例加深一下印象。

	//获取第二个参数,一般是键值
	o = lookupKeyWrite(c->db,c->argv[1]);
	
	//获取第三个参数
	getLongLongFromObjectOrReply(c,c->argv[2],&start,NULL)
	
	//获取第四个参数
	getLongLongFromObjectOrReply(c,c->argv[3],&end,NULL)

	//从第二个参数遍历所有的参数
    for (j = 1; j < c->argc; j++) {
   
     
            robj *o = lookupKeyRead(c->db,c->argv[j]);
        if (o == NULL) {
   
     
            addReply(c,shared.nullbulk);
        } else {
   
     
            if (o->type != REDIS_STRING) {
   
     
                addReply(c,shared.nullbulk);
            } else {
   
     
                addReplyBulk(c,o);
            }
        }
    }

6 事件通知

在很多方法的结尾,我们能看到 notifyKeyspaceEvent 这个方法,这个方法定义在notify.c文件中,主要功能就是做一个事件通知,告知客户端一些信息,等到后面学习这个文件的时候我们再详细研究,这里先看下我们学习的几个文件都做了哪些通知。

6.1 t_string.c

	notifyKeyspaceEvent(REDIS_NOTIFY_STRING,"set",key,c->db->id);

	notifyKeyspaceEvent(REDIS_NOTIFY_STRING,"setrange",c->argv[1],c->db->id);

	notifyKeyspaceEvent(REDIS_NOTIFY_STRING,"incrby",c->argv[1],c->db->id);

	notifyKeyspaceEvent(REDIS_NOTIFY_STRING,"incrbyfloat",c->argv[1],c->db->id);

	notifyKeyspaceEvent(REDIS_NOTIFY_STRING,"append",c->argv[1],c->db->id);

	notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC, "expire",key,c->db->id);

6.2 t_list.c

	char *event = (where == REDIS_HEAD) ? "lpush" : "rpush";
	notifyKeyspaceEvent(REDIS_NOTIFY_LIST,event,c->argv[1],c->db->id);

	char *event = (where == REDIS_HEAD) ? "lpop" : "rpop";
	notifyKeyspaceEvent(REDIS_NOTIFY_LIST,event,c->argv[1],c->db->id);
	notifyKeyspaceEvent(REDIS_NOTIFY_LIST,"linsert", c->argv[1],c->db->id);

	notifyKeyspaceEvent(REDIS_NOTIFY_LIST,"lset",c->argv[1],c->db->id);

	notifyKeyspaceEvent(REDIS_NOTIFY_LIST,"ltrim",c->argv[1],c->db->id);

	notifyKeyspaceEvent(REDIS_NOTIFY_LIST,"lpush",dstkey,c->db->id);

	notifyKeyspaceEvent(REDIS_NOTIFY_LIST,"rpop",touchedkey,c->db->id);

	//这里的类型其实是有问题的,讲道理应该是REDIS_NOTIFY_LIST。
	//为了验证猜想,特意去看了6.0版本的代码,发现6.0版本果然还是改成了LIST。
	notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"lrem",c->argv[1],c->db->id);

	notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"del",c->argv[1],c->db->id);
	

6.3 t_hash.c


	notifyKeyspaceEvent(REDIS_NOTIFY_HASH,"hset",c->argv[1],c->db->id);

	notifyKeyspaceEvent(REDIS_NOTIFY_HASH,"hincrby",c->argv[1],c->db->id);
	
	notifyKeyspaceEvent(REDIS_NOTIFY_HASH,"hincrbyfloat",c->argv[1],c->db->id);
	
	notifyKeyspaceEvent(REDIS_NOTIFY_HASH,"hdel",c->argv[1],c->db->id);
	

6.4 t_set.c


	notifyKeyspaceEvent(REDIS_NOTIFY_SET,"sadd",c->argv[1],c->db->id);
	
	notifyKeyspaceEvent(REDIS_NOTIFY_SET,"srem",c->argv[1],c->db->id);
		
	notifyKeyspaceEvent(REDIS_NOTIFY_SET,"spop",c->argv[1],c->db->id);
		
	notifyKeyspaceEvent(REDIS_NOTIFY_SET,"sinterstore",dstkey,c->db->id);
		
	

6.5 t_zset.c

	
	notifyKeyspaceEvent(REDIS_NOTIFY_ZSET,incr ? "zincr" : "zadd", key, c->db->id);
		
	notifyKeyspaceEvent(REDIS_NOTIFY_ZSET,"zrem",key,c->db->id);
		
	char *event[3] = {
   
     "zremrangebyrank","zremrangebyscore","zremrangebylex"};
	notifyKeyspaceEvent(REDIS_NOTIFY_ZSET,event[rangetype],key,c->db->id);
		
	notifyKeyspaceEvent(REDIS_NOTIFY_ZSET,
            (op == REDIS_OP_UNION) ? "zunionstore" : "zinterstore",
            dstkey,c->db->id);
    

6.6 总结

1、 notifyKeyspaceEvent传入的参数有类型、事件名称、键值、数据库id;
2、 notifyKeyspaceEvent顾名思义是键空间事件,在根据传入的参数,可以推测出是针对某个键的操作的通知;
3、 一般能触发事件通知的都是一些增删改的操作;
4、 不同的数据类型事件通知,传入的第一个类型参数是不同的;
5、 通用的命令操作,传入的类型参数是REDIS_NOTIFY_GENERIC,事件有del、expire;

7 通用逻辑流程

经过一系列源代码的阅读,其实我们是能够总结出一个方法的通用逻辑流程,基本上大部分的方法都按照这个逻辑流程处理。

1、 通过c->argv获取输入参数,并校验参数是否符合相应的类型;
2、 通过lookupKey相关方法,根据键值获取相应的redis对象;
3、 根据当前命令调用相关的API方法执行具体逻辑;
4、 一般API方法里都会包含两种数据结构的处理逻辑,根据当前的数据编码来选择具体的逻辑;
5、 根据当前数据编码和操作命令,触发事件通知;
6、 调用addReply相关方法返回相应的执行结果;

8 总结

1、 数据类型有五种,字符串、列表、哈希、集合、有序集合;
2、 对应的源代码文件分别是t_string.c、t_list.c、t_hash.c、t_set.c、t_zset.c;
3、 redisObject可以用来存储以上几种数据类型;
4、 这几个源代码一般将方法分为两大类,一类是API,一类是方法;
5、 对键做了修改操作,会调用notifyKeyspaceEvent方法触发相应事件的通知;