redis的内部内部整体的数据结构就像是一个巨大的hashmap,内部的实现是数组实现 hash,如果发生冲突通过挂链去实现,然后每个dictEntry就是一个key/value对 象。dictEntry的key指向set key value命令中的key对应的对象,dictEntry的v 指向set key value命令中的value对应的对象。
在dictEntry内部包含了数据存储的key和v的值,同时包含一个dictEntry的next指 针连接落入同一个hash的对象。dictEntry的key和v的指针是指向的是 redisObject。
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 指向下个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
上面说到了redisObject,那么redisObject到底是什么呢?是redis server存储 最原子数据的数据结构。其中的void *ptr会指向真正的存储数据结构,我们set key value中的key和value其实由ptr指向真正保存的位置。
typedef struct redisObject {
// 类型
unsigned type:4;
// 编码
unsigned encoding:4;
// 对象最后一次被访问的时间
unsigned lru:REDIS_LRU_BITS;
// 引用计数
int refcount;
// 指向实际值的指针
void *ptr;
} robj;
redis String类型转换
我们可能会认为redis在内部存储string都是用sds的数据结构实现的,其实在整个redis的数据存储过程中为了提高性能,内部做了很多优化。整体选择顺序应该是: 1. 整数,存储字符串长度小于21且能够转化为整数的字符串。 2. EmbeddedString,存储字符串长度小于39的字符串(REDIS_ENCODING_EMBSTR_SIZE_LIMIT)。 3. SDS,剩余情况使用sds进行存储。
embstr和sds的区别在于内存的申请和回收
1. embstr的创建只需分配一次内存,而raw为两次(一次为sds分配对象,另一次为 redisObject分配对象,embstr省去了第一次)。相对地,释放内存的次数也由两次变为一 次。 2. embstr的redisObject和sds放在一起,更好地利用缓存带来的优势 3. 缺点:redis并未提供任何修改embstr的方式,即embstr是只读的形式。对embstr的修改实际上是先转换为raw再进行修改。String编码转换源码
通过redis 内部的命令映射表我们找到set对应的处理函数为setCommand,相当于 这个是处理set命令的入口函数,关注下tryObjectEncoding,内部对其实对 Object进行转换。
void setCommand(redisClient *c) {
int j;
robj *expire = NULL;
int unit = UNIT_SECONDS;
int flags = REDIS_SET_NO_FLAGS;
// 设置选项参数
for (j = 3; j < c->argc; j++) {
char *a = c->argv[j]->ptr;
robj *next = (j == c->argc-1) ? NULL : c->argv[j+1];
if ((a[0] == 'n' || a[0] == 'N') &&
(a[1] == 'x' || a[1] == 'X') && a[2] == ' ') {
flags |= REDIS_SET_NX;
} else if ((a[0] == 'x' || a[0] == 'X') &&
(a[1] == 'x' || a[1] == 'X') && a[2] == ' ') {
flags |= REDIS_SET_XX;
} else if ((a[0] == 'e' || a[0] == 'E') &&
(a[1] == 'x' || a[1] == 'X') && a[2] == ' ' && next) {
unit = UNIT_SECONDS;
expire = next;
j++;
} else if ((a[0] == 'p' || a[0] == 'P') &&
(a[1] == 'x' || a[1] == 'X') && a[2] == ' ' && next) {
unit = UNIT_MILLISECONDS;
expire = next;
j++;
} else {
addReply(c,shared.syntaxerr);
return;
}
}
// 尝试对值对象进行编码
c->argv[2] = tryObjectEncoding(c->argv[2]);
setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL);
}
整个尝试编码转换的逻辑过程通过代码的注释应该是比较清楚了,过程如下:
只对长度小于或等于 21 字节,并且可以被解释为整数的字符串进行编码,使用整数存储尝试将 RAW 编码的字符串编码为 EMBSTR 编码,使用EMBSTR 编码这个对象没办法进行编码,尝试从 SDS 中移除所有空余空间,使用SDS编码
// 尝试对字符串对象进行编码,以节约内存。
robj *tryObjectEncoding(robj *o) {
long value;
sds s = o->ptr;
size_t len;
redisAssertWithInfo(NULL,o,o->type == REDIS_STRING);
// 只在字符串的编码为 RAW 或者 EMBSTR 时尝试进行编码
if (!sdsEncodedObject(o)) return o;
// 不对共享对象进行编码
if (o->refcount > 1) return o;
// 对字符串进行检查
// 只对长度小于或等于 21 字节,并且可以被解释为整数的字符串进行编码
len = sdslen(s);
if (len <= 21 && string2l(s,len,&value)) {
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);
o->encoding = REDIS_ENCODING_INT;
o->ptr = (void*) value;
return o;
}
}
// 尝试将 RAW 编码的字符串编码为 EMBSTR 编码
if (len <= REDIS_ENCODING_EMBSTR_SIZE_LIMIT) {
robj *emb;
if (o->encoding == REDIS_ENCODING_EMBSTR) return o;
emb = createEmbeddedStringObject(s,sdslen(s));
decrRefCount(o);
return emb;
}
// 这个对象没办法进行编码,尝试从 SDS 中移除所有空余空间
if (o->encoding == REDIS_ENCODING_RAW &&
sdsavail(s) > len/10)
{
o->ptr = sdsRemoveFreeSpace(o->ptr);
}
return o;
}
redis sds的介绍
在C语言中,字符串可以用' '结尾的char数组标示。这种简单的字符串表示,在大 多数情况下都能满足要求,但是不能高效的计算length和append数据。所以Redis自 己实现了SDS(简单动态字符串)的抽象类型。 SDS的数据结构如下,len表示sdshdr中数据的长度,free表示sdshdr中剩余的空 间,buf表示实际存储数据的空间。 sdslen的函数有一个细节需要我们注意,那就是通过(s-(sizeof(struct sdshdr)))来计算偏移量,之所以需要这么计算是因为sds的指针指向的是char buf[]位置,所以我们需要访问sdshdr的首地址的时候需要减去偏移量。
struct sdshdr {
// buf 中已占用空间的长度
int len;
// buf 中剩余可用空间的长度
int free;
// 数据空间
char buf[];
};
static inline size_t sdslen(const sds s) {
struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
return sh->len;
}
static inline size_t sdsavail(const sds s) {
struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
return sh->free;
}
redis list底层数据结构
基础介绍
redis list数据结构底层采用压缩列表ziplist或linkedlist两种数据结构进行存 储,首先以ziplist进行存储,在不满足ziplist的存储要求后转换为linkedlist列 表。 当列表对象同时满足以下两个条件时,列表对象使用ziplist进行存储,否则用 linkedlist存储。
列表对象保存的所有字符串元素的长度小于64字节列表对象保存的元素数量小于512个。 list元素添加过程
list的数据添加根据传入的变量个数一个个顺序添加,整个顺序如下: * 创建list对象并添加到db的数据结构当中 * 针对每个待插入的元素添加到list当中
void pushGenericCommand(redisClient *c, int where) {
int j, waiting = 0, pushed = 0;
// 取出列表对象
robj *lobj = lookupKeyWrite(c->db,c->argv[1]);
// 如果列表对象不存在,那么可能有客户端在等待这个键的出现
int may_have_waiting_clients = (lobj == NULL);
if (lobj && lobj->type != REDIS_LIST) {
addReply(c,shared.wrongtypeerr);
return;
}
// 将列表状态设置为就绪
if (may_have_waiting_clients) signalListAsReady(c,c->argv[1]);
// 遍历所有输入值,并将它们添加到列表中
for (j = 2; j < c->argc; j++) {
// 编码值
c->argv[j] = tryObjectEncoding(c->argv[j]);
// 如果列表对象不存在,那么创建一个,并关联到数据库
if (!lobj) {
lobj = createZiplistObject();
dbAdd(c->db,c->argv[1],lobj);
}
// 将值推入到列表
listTypePush(lobj,c->argv[j],where);
pushed++;
}
// 返回添加的节点数量
addReplyLongLong(c, waiting + (lobj ? listTypeLength(lobj) : 0));
// 如果至少有一个元素被成功推入,那么执行以下代码
if (pushed) {
char *event = (where == REDIS_HEAD) ? "lpush" : "rpush";
// 发送键修改信号
signalModifiedKey(c->db,c->argv[1]);
// 发送事件通知
notifyKeyspaceEvent(REDIS_NOTIFY_LIST,event,c->argv[1],c->db->id);
}
server.dirty += pushed;
}
list的每个元素的插入过程中,我们会对是否需要进行转码作两个判断:
对每个插入元素的长度进行判断是否进行ziplist->linkedlist的转码。对list总长度是否超过ziplist最大长度的判断。



