redis字符串实现

在使用redis的时候经常使用到了字符串,比如下面的语句

SET user:id:100 {“name”: “zhangsan”, “gender”: “M”,“city”:"beijing"}

先来看看C语言原生的char存在的几个问题
1 存储二进制
char
是分配了一段连续的内存空间来存储字符串,并且在结尾是一’\0’,那么如果在保存数据的时候出现了\0,就会被截断,不符合保存二进制数据的需求
2 操作函数复杂度
char* 在计算长度,追加字符串的时候需要遍历整个字符串,这样子整个时间复杂度是o(N),在大量操作字符串的时候会消耗大量的时间,比如strcat的实现:

  char *strcat(char *dest, const char *src) {
     //将目标字符串复制给tmp变量
     char *tmp = dest;
     //用一个while循环遍历目标字符串,直到遇到“\0”跳出循环,指向目标字符串的末尾
     while(*dest)
        dest++;
     //将源字符串中的每个字符逐一赋值到目标字符串中,直到遇到结束字符
     while((*dest++ = *src++) != '\0' )
     return tmp;
  }

为了解决上面两个问题,提升效率,redis采用了sds的设计,分配sds字符串的实现

//创建SDS字符串
sds _sdsnewlen(const void *init, size_t initlen, int trymalloc) {
    void *sh;
    sds s;
    //sds字符串类型
    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;
    //sds 头部长度
    int hdrlen = sdsHdrSize(type);
    unsigned char *fp; /* flags pointer. */
    size_t usable;
    //判断是否溢出
    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);
    //分配失败
    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);
    //判断SDS的类型,根据类型分配头部
    switch(type) {
        case SDS_TYPE_5: {
            *fp = type | (initlen << SDS_TYPE_BITS);
            break;
        }
        case SDS_TYPE_8: {
            SDS_HDR_VAR(8,s);
            sh->len = initlen;
            sh->alloc = usable;
            *fp = type;
            break;
        }
        case SDS_TYPE_16: {
            SDS_HDR_VAR(16,s);
            sh->len = initlen;
            sh->alloc = usable;
            *fp = type;
            break;
        }
        case SDS_TYPE_32: {
            SDS_HDR_VAR(32,s);
            sh->len = initlen;
            sh->alloc = usable;
            *fp = type;
            break;
        }
        case SDS_TYPE_64: {
            SDS_HDR_VAR(64,s);
            sh->len = initlen;
            sh->alloc = usable;
            *fp = type;
            break;
        }
    }
    //拷贝字符串
    if (initlen && init)
        memcpy(s, init, initlen);
    //最后一位设置为\0
    s[initlen] = '\0';
    return s;
}

可以看到SDS 本质还是字符数组,只是在字符数组基础上增加了额外的元数据,所以直接用了SDS这个别名

typedef char *sds;

SDS的优势体现在
1 提升效率
在追加字符串的时候,不需要在遍历整个字符串,只要3步:
1 保证存储空间的大小 sdsMakeRoomFor
2 拷贝字符串 memcpy
3 设置目标字符串的新长度 sdssetlen
整个提升了操作字符串的效率。

//SDS追加字符串
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);
//  最后一位设置为\0
    s[curlen+len] = '\0';
    return s;
}

2 方便的错误检查
sdsMakeRoomFor实现了空间检查和扩容封装,避免了开发人员忘记给目标字符串扩容导致操作失败的情况,避免了没有做检查出现的内存溢出的情况。

sds _sdsMakeRoomFor(sds s, size_t addlen, int greedy) {
    void *sh, *newsh;
    //可用空间
    size_t avail = sdsavail(s);
    size_t len, newlen, reqlen;
    char type, oldtype = s[-1] & SDS_TYPE_MASK;
    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);
    //新长度
    reqlen = newlen = (len+addlen);
    assert(newlen > len);   /* Catch size_t overflow */
    //判断扩展方式,是2倍的扩展,还是1倍的扩展
    if (greedy == 1) {
        if (newlen < SDS_MAX_PREALLOC)
            newlen *= 2;
        else
            newlen += SDS_MAX_PREALLOC;
    }
    //因为增加了长度,需要判断现在的sds的类型是什么
    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 > reqlen);  /* 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;
}

3 实现了保存二进制的需求
因为sds的头部指明了长度,不在以\0结尾计算,实现了保存二进制数据的需求。
4 节省内存的优秀设计
sds设计了5种类型,分别是 sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64。
区别是len和alloc的数据类型不同。
采用了__attribute__的内存分配方式,不要使用字节对齐的方式,而是采用紧凑的方式分配内存。编译器会按需分配空间,而不是按照8字节对齐分配空间。

//__attribute__ 采用紧凑的方式分配内存,减少内存的使用
struct __attribute__ ((__packed__)) sdshdr8 {
....
}
struct __attribute__ ((__packed__)) sdshdr8 {
//  字符串现有长度 使用的数据类型uint8_t
    uint8_t len; /* used */
//    字符串已分配空间 使用的数据类型uint8_t
    uint8_t alloc; /* excluding the header and null terminator */
//    SDS类型
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
//    内容
    char buf[];
};

struct __attribute__ ((__packed__)) sdshdr16 {
// 数据类型uint16_t
    uint16_t len; /* used */
// 数据类型uint16_t
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
//获取SDS的类型长度
static inline size_t sdslen(const sds s) {
    unsigned char flags = s[-1];
    switch(flags&SDS_TYPE_MASK) {
        case SDS_TYPE_5:
            return SDS_TYPE_5_LEN(flags);
        case SDS_TYPE_8:
            return SDS_HDR(8,s)->len;
        case SDS_TYPE_16:
            return SDS_HDR(16,s)->len;
        case SDS_TYPE_32:
            return SDS_HDR(32,s)->len;
        case SDS_TYPE_64:
            return SDS_HDR(64,s)->len;
    }
    return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Redis 字符串Redis 中最基本的数据类型。它是一种键值对存储方式,键是字符串类型,值也是字符串类型。 Redis 字符串的底层实现是基于双向链表和字典(dictionary)的。在 Redis 中,所有的键值对都存储在一个字典中,字典中的每一个节点都是一个键值对,同时也是一个双向链表的节点。字典本身是一个哈希表,用于快速查找和插入键值对。 当 Redis 中的一个字符串被修改时,Redis 会将旧的字符串值从字典中删除,然后将新的字符串值插入到字典中。这样,就可以保证 Redis 字符串的原子性,同时也保证了字符串的高效存储。 总结一下,Redis 字符串的底层原理就是基于字典和双向链表实现的键值对存储方式。 ### 回答2: Redis 字符串的底层实现原理是基于简单动态字符串(SDS)和字典(dict)。 简单动态字符串(SDS)是 Redis 底层的字符串实现,它是一个动态分配的字符数组,并且可以在 O(1) 复杂度下进行字符串长度的获取和修改。SDS 的结构体中包含字符串指针、字符串长度、已分配内存长度等字段,通过这些字段可以方便地对字符串进行操作。 字典(dict)是 Redis 底层用于存储字符串键值对的数据结构。在 Redis 字符串中,键相当于字符串的名字,值则是存储的实际数据。字典采用哈希表作为底层实现,使用哈希函数将键映射到哈希桶中,以提高查找效率。在 Redis 中,哈希表的长度会根据实际数据的增加和删除进行动态扩容和缩容,以保证哈希表的平均负载因子不超过一个特定的值。 Redis 字符串的底层实现成为一个 SDS 字符串结构,它与字典结构之间是相互独立的。当一个字符串被确定为一个键或值时,它会被存储在一个 SDSDICT 字典中,其中键为字符串本身,值则是一个指向 SDS 结构的指针。 总结来说,Redis 字符串的底层实现原理是基于简单动态字符串(SDS)和字典(dict)。SDS 是一个动态分配的字符数组,可以方便地进行字符串长度的获取和修改。而字典用于存储字符串键值对,通过哈希表提高查找效率。在 Redis 中,字符串被存储在一个 SDSDICT 字典中,其中键为字符串本身,值为指向 SDS 结构的指针。 ### 回答3: Redis字符串的底层原理是通过使用简单动态字符串(简称SDS)实现的。SDS是Redis自己实现的以C字符串结构为基础的字符串库,它解决了C字符串的一些限制,使得Redis可以支持更多的操作和功能。 在Redis中,每个字符串对象都由一个redisObject结构表示,该结构包含了一个指向SDS的指针和其他元数据。SDS结构由以下几部分组成: 1. len:记录字符串的长度,即字节数。 2. free:记录SDS结尾未使用的字节数,方便扩展字符串时无需重新分配内存。 3. buf:实际的字符数组,用于存储字符串的内容。 Redis字符串对象的底层原理有以下几个特点: 1. 动态扩展:SDS提供了高效的内存扩展机制,当字符串长度增加时,可以动态调整内存大小,避免了频繁的内存重新分配操作,提高了性能。 2. O(1)时间复杂度:SDS支持通过偏移量来直接访问字符串的某一位置的字符,所以读取和修改字符串的某一位置的操作时间复杂度为O(1)。 3. 惰性空间释放:当从字符串中删除部分字符时,SDS并不立即释放所占用的内存,而是通过将free字段增加相应的值来标记该内存已被释放,以备将来再次使用。 4. 兼容C字符串:SDS结构与C字符串之间可以相互转换,方便Redis与其他系统进行兼容。 总的来说,Redis字符串的底层原理是通过使用SDS实现的,SDS提供了高效的内存扩展和访问机制,使得Redis可以高效地处理字符串操作,提高了性能和灵活性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值