在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[value]); return shared.integers[value]; } 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[1] 对应的值对象 // 如果键不存在时,向客户端发送回复信息,并返回 NULL 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; } 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””。