redis 操作命令和字符串sds源码分析

被天美面试官怼了没有技术深度,确实看源码看的少,趁着毕业前看看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

后续发现什么在更新

猜你喜欢

转载自blog.csdn.net/dxgzg/article/details/121612097