renyanping 发表于 2016-12-20 10:11:00

redis之字符串命令源码解析(一)

  形象化设计模式实战             HELLO!架构
  在redis的使用中,set/get无疑是使用最普遍的命令,我先telnet连接运行看看

  先看get命令,获取一个key服务器返回了两行内容,是"$3\r\n123\r\n"(\r\n为换行符),不难发现3就是“123”的长度,redis的官方文档get返回值为:
Bulk string reply: the value of key, or nil when key does not exist.
  可以点击超链接看里面的解释,发现确实如此,那现在就从源码看看get是如何获取数据的。

1、内部数据结构之sds(Simple Dynamic String)
  在传统C语言中,表示字符串通常是char *,由于char *类型的功能单一,抽象层次低,并且不能高效地支持一些Redis常用的操作(比如追加操作和长度计算操作),所以在Redis程序内部,绝大部分情况下都会使用sds而不是char *来表示字符串
  sds的结构

现假如运行命令 set test "hello redis"
  那么set命令创建并保存"test"到一个sdshdr中:(最终保存到数据库是char *类型,指向sdshdr->buf)

struct sdshdr
{
len= 4;
free = 0;
buf = "test\0";
};
 
  将"hello redis"保存到另一个sdshdr中:(最终保存到数据库是robj类型,后序会讲解)

struct sdshdr
{
len= 11;
free = 0;
buf = "hello redis\0";
};
  那么如果再运行append test " now!",那是不是就会变成

struct sdshdr
{
len= 16;
free = 0;
buf = "hello redis now!\0";
};
  这样呢?不是!
  sdsMakeRoomFor函数描述此场景的内存预分配优化策略

/* Enlarge the free space at the end of the sds string so that the caller
* is sure that after calling this function can overwrite up to addlen
* bytes after the end of the string, plus one more byte for nul term.
*
* Note: this does not change the *length* of the sds string as returned
* by sdslen(), but only the free buffer space we have. */
/*
* 对 sds 中 buf 的长度进行扩展,确保在函数执行之后,
* buf 至少会有 addlen + 1 长度的空余空间
* (额外的 1 字节是为 \0 准备的)
*
* 返回值
*sds :扩展成功返回扩展后的 sds
*      扩展失败返回 NULL
*
* 复杂度
*T = O(N)
*/
sds sdsMakeRoomFor(sds s, size_t addlen) {
struct sdshdr *sh, *newsh;
// 获取 s 目前的空余空间长度
size_t free = sdsavail(s);
size_t len, newlen;
// s 目前的空余空间已经足够,无须再进行扩展,直接返回
if (free >= addlen) return s;
// 获取 s 目前已占用空间的长度
len = sdslen(s);
sh = (void*) (s-(sizeof(struct sdshdr)));
// s 最少需要的长度
newlen = (len+addlen);
// 根据新长度,为 s 分配新空间所需的大小
if (newlen < SDS_MAX_PREALLOC)
// 如果新长度小于 SDS_MAX_PREALLOC(1024*1024)
// 那么为它分配两倍于所需长度的空间
newlen *= 2;
else
// 否则,分配长度为目前长度加上 SDS_MAX_PREALLOC
newlen += SDS_MAX_PREALLOC;
// T = O(N)
newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);
// 内存不足,分配失败,返回
if (newsh == NULL) return NULL;
// 更新 sds 的空余长度
newsh->free = newlen - len;
// 返回 sds
return newsh->buf;
}
  很明显在append后,test的长度只有15,远远不够1024*1024的,所以它的新长度应该是16*2+1=31

struct sdshdr
{
len= 33;
free = 16;
buf = "hello redis now!\0";
};

2、字符串编码
  上面说set命令会将字符串数据保存在sdshdr中,那如果是一个数字也会如此吗?答案是不会!
  object.c的tryObjectEncoding方法

/* 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 的字符串进行编码
// 也不对可以被解释为整数的字符串进行编码
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. */
if (server.maxmemory == 0 &&
value >= 0 &&
value < REDIS_SHARED_INTEGERS)
{
decrRefCount(o);
incrRefCount(shared.integers);
return shared.integers;
} else {
if (o->encoding == REDIS_ENCODING_RAW) sdsfree(o->ptr);
//将encoding转为REDIS_ENCODING_INT
o->encoding = REDIS_ENCODING_INT;
o->ptr = (void*) value;
return o;
}
}
 
  由此可见,字符串类型有两种编码:
  1、REDIS_ENCODING_INT使用long类型来保存long类型值
  2、REDIS_ENCODING_RAW 使用sdshdr结构来保存sds(也就是char *)、long long double和long double类型值。

3、get命令的实现
  t_string.c中

int getGenericCommand(redisClient *c) {
robj *o;
// 尝试从数据库中取出键 c->argv 对应的值对象
// 如果键不存在时,向客户端发送回复信息,并返回 NULL
if ((o = lookupKeyReadOrReply(c,c->argv,shared.nullbulk)) == NULL)
return REDIS_OK;
// 值对象存在,检查它的类型
if (o->type != REDIS_STRING) {
// 类型错误
addReply(c,shared.wrongtypeerr);
return REDIS_ERR;
} else {
// 类型正确,向客户端返回对象的值
addReplyBulk(c,o);
return REDIS_OK;
}
}
  这里说明一点,redis的key/value都是由“字典”数据结构实现,在这里不做深究。

/* Add a Redis Object as a bulk reply
*
* 返回一个 Redis 对象作为回复
*/
void addReplyBulk(redisClient *c, robj *obj) {
//回复字符的长度
addReplyBulkLen(c,obj);
//回复要返回的字符
addReply(c,obj);
//回复"\r\n"
addReply(c,shared.crlf);
}
  这样就出现了开始的“"$3\r\n123\r\n"”。
页: [1]
查看完整版本: redis之字符串命令源码解析(一)