被天美面试官怼了没有技术深度,确实看源码看的少,趁着毕业前看看redis的源码。
操作命令
Get、Set、mset、mget
后面nx表示不存在key才创建,xx表示key存在才可以修改。
mset nx 如果有一个key存在那么这条命令就不对了。
strlen获得字符串,时间复杂度是O(1)
getrange 获取范围字符串,支持正索引和负索引值
setrange,在范围内赋值,如果位数不够0来补齐
append 追加新的内容到字符串尾部
incrby decrby incr decr incrbyfloat,前四个是整数,最后一个是浮点数,没有提供decrbyfloat,可以使用incrbyfloat key -3.14,加上一个负数来实现减法。和APPEND一样,处理不存在的key时候都会自动创建。
sds设计
字符串安全,char*字符串可以保存\0,记录分配的空间,也减少了内存重新分配的次数,惰性空间的释放等等。
源码
下面是一些配置版本
centos8
g++11
redis6.2.1
__attribute__ ((__packed__))
这个是什么呢?,就是取消了字节对齐,压缩内存空间,malloc每次分配会是8字节的倍数(64字节的情况),所以我们可以使用这个属性来取消无用的字节对齐,这里我也新学到了一个函数,malloc_usable_size,可以看到系统实际分配的字节数。还有一点需要记住,char*字符串是没有大端小端这一说法的,地址是按照顺序依次增大排下来的,深入理解计算机系统(第三版)有讲,阅读两天实在没有搞清楚这个字节序,突然想到了曾经看到的内容,问题就解决了。
介绍一下神奇的指针
一提到指针确实没少用,但确实没想到指针可以这么用,这也是读下面源码例子的必须掌握的点,
#include <string>
#include <iostream>
#include <unordered_map>
#include <memory>
#include <functional>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <vector>
#include <map>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
using namespace std;
using byte_pointer = unsigned char*;
void show_bytes(byte_pointer start,size_t len){
for(size_t i = 0;i < len;++i){
printf("%.2x ",start[i]);
}
cout << endl;
}
int main()
{
void* ptr = malloc(1);
memset(ptr,0,1);
printf("%u\n",malloc_usable_size(ptr));
size_t size = 5;
*((size_t*)ptr) = size;
cout << *((size_t*)ptr) << endl;
memcpy(ptr + 8,"hello dxgzg",11);
cout << (char*)ptr + 8 << endl;
cout << *((size_t*)ptr) << endl;
show_bytes((byte_pointer)ptr,19);
return 0;
}
输出的结果如下图所示,可以看到size_t是按照小端方式存储的,而字符串并不是小端这样的。
这就是一个数字和字符串结合的例子。
下面这个例子就是类对象与字符串的例子,在利用sizeof来获得偏移量。
#include <string>
#include <iostream>
#include <unordered_map>
#include <memory>
#include <functional>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <vector>
#include <map>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
using namespace std;
using byte_pointer = unsigned char*;
void show_bytes(byte_pointer start,size_t len){
for(size_t i = 0;i < len;++i){
printf("%.2x ",start[i]);
}
cout << endl;
}
struct __attribute__((__packed__)) test
{
char i = 'a';
int val = 0;
};
int main()
{
void* ptr = malloc(1);
// printf("%u\n",malloc_usable_size(ptr));
memset(ptr,0,malloc_usable_size(ptr));
test* t = (test*)ptr;
t->i = 'd';
t->val = 10;
memcpy(ptr + sizeof(test),"hello world",11);
show_bytes((byte_pointer)&ptr,16);
test* t2 = (test*)ptr;
cout << t2->i << ' ' << t2->val << endl;
cout << (char*)ptr + sizeof(test) << endl;
return 0;
}
sdnew函数
最终调用的就是这个函数。
sds sdsnewlen(const void *init, size_t initlen) {
return _sdsnewlen(init, initlen, 0);
}
sds的内存布局
sh和s都是指针
这个变量的意思
下面就是_sdsnewlen,来剖析一下他,type就是记录一下这个字符串是那种sdshdr.hdrlen就是看看那种sdshdr的大小。
sh分配了一块空间(空间大小是hdrlen+initlen+1+PREFIX_SIZE),
hdrlen长度存储的是sdshdrX里的成员(X表示5、8、16、32、64)
sh中的flag主要是帮助我们快速找到sh的地址,因为我们操作的一直都是s的地址,而这个flag的地址就存在s - 1的地方。已知s的位置,又知道减去多少(flag就可以得知)得到sh位置
实际分配的大小存储在usable,就是根据上面那个函数来的。s_malloc_usable函数最终调用ztrymalloc_usable函数,里面有一个宏是HAVE_MALLOC_SIZE,这个宏就是来判断当前系统是否含有malloc_usable_size这个函数,没有的话,redis自己来记录分配的大小。如果HAVE_MALLOC_SIZE定义了,PREFIX_SIZE就是0了
sds _sdsnewlen(const void *init, size_t initlen, int trymalloc) {
void *sh;
sds s;
char type = sdsReqType(initlen);
/* Empty strings are usually created in order to append. Use type 8
* since type 5 is not good at this. */
if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
int hdrlen = sdsHdrSize(type);// 结构体的大小
unsigned char *fp; /* flags pointer. */
size_t usable; // malloc实际分配的值
assert(initlen + hdrlen + 1 > initlen); /* Catch size_t overflow */
sh = trymalloc?
s_trymalloc_usable(hdrlen+initlen+1, &usable) :
s_malloc_usable(hdrlen+initlen+1, &usable); // +1是留给\0的,还会多分配PREFIX_SIZE字节
if (sh == NULL) return NULL;
if (init==SDS_NOINIT)
init = NULL;
else if (!init)
memset(sh, 0, hdrlen+initlen+1);
s = (char*)sh+hdrlen;
fp = ((unsigned char*)s)-1;
usable = usable-hdrlen-1;
if (usable > sdsTypeMaxSize(type))
usable = sdsTypeMaxSize(type);
switch(type) {
case SDS_TYPE_5: {
*fp = type | (initlen << SDS_TYPE_BITS);
break;
}
case SDS_TYPE_8: {
SDS_HDR_VAR(8,s);
// 相当于插入这样的代码了
// struct sdshdr8 *sh = (void*)((s)-(sizeof(struct sdshdr8)));
sh->len = initlen;
sh->alloc = usable;
*fp = type;
break;
}
.............
}
if (initlen && init)
memcpy(s, init, initlen);
s[initlen] = '\0';
return s;
}
sdsfree函数
释放空间 这个和上述函数同理,可以自行看懂了
sdsavail函数
获取未分配的值,这个也是操作sds,通过s - 1找到flag,然后找到对应sh,sh->alloc就是以分配的,len就是已使用,做个减法就知道剩余没有用的空间了。
static inline size_t sdsavail(const sds s) {
unsigned char flags = s[-1];
switch(flags&SDS_TYPE_MASK) {
case SDS_TYPE_5: {
return 0;
}
case SDS_TYPE_8: {
SDS_HDR_VAR(8,s);
return sh->alloc - sh->len;
}
........
}
return 0;
}
sdscat函数
将一个字符串拼接到sds后面,就是redis的append命令,利用的memcpy来进行拼接字符串的。sdssetlen更新新的剩余空间。sdsMakeRoomFor来进行判断是否需要扩容。
sds sdscatlen(sds s, const void *t, size_t len) {
size_t curlen = sdslen(s);
s = sdsMakeRoomFor(s,len);
if (s == NULL) return NULL;
memcpy(s+curlen, t, len);
sdssetlen(s, curlen+len);
s[curlen+len] = '\0';
return s;
}
sdsMakeRoomFor
sdsMakeRoomFor函数的原理,如果小于1MB就是新长度的二倍,如果是大于1MB,就多扩容1MB。
sds sdsMakeRoomFor(sds s, size_t addlen) {
void *sh, *newsh;
size_t avail = sdsavail(s);
size_t len, newlen;
char type, oldtype = s[-1] & SDS_TYPE_MASK;// type只是声明了
int hdrlen;
size_t usable;
/* Return ASAP if there is enough space left. */
if (avail >= addlen) return s;
len = sdslen(s);
sh = (char*)s-sdsHdrSize(oldtype);
newlen = (len+addlen);
assert(newlen > len); /* Catch size_t overflow */
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;
type = sdsReqType(newlen);
/* Don't use type 5: the user is appending to the string and type 5 is
* not able to remember empty space, so sdsMakeRoomFor() must be called
* at every appending operation. */
if (type == SDS_TYPE_5) type = SDS_TYPE_8;
hdrlen = sdsHdrSize(type);
assert(hdrlen + newlen + 1 > len); /* Catch size_t overflow */
if (oldtype==type) {
newsh = s_realloc_usable(sh, hdrlen+newlen+1, &usable);
if (newsh == NULL) return NULL;
s = (char*)newsh+hdrlen;
} else {
/* Since the header size changes, need to move the string forward,
* and can't use realloc */
newsh = s_malloc_usable(hdrlen+newlen+1, &usable);
if (newsh == NULL) return NULL;
memcpy((char*)newsh+hdrlen, s, len+1);
s_free(sh);
s = (char*)newsh+hdrlen;
s[-1] = type;
sdssetlen(s, len);
}
usable = usable-hdrlen-1;
if (usable > sdsTypeMaxSize(type))
usable = sdsTypeMaxSize(type);
sdssetalloc(s, usable);
return s;
}
发现需要扩容的时候如果新的type还是sds5,那么防止下次再增加还需要扩容直接提升到sds8。如果类型发生了改变需要free之前的在重新malloc,因为hdrlen也随着结构改变而改变了。在设置新的sh的alloc和len
后续发现什么在更新