Redis原理解析(一) String底层实现之SDS

本系列所有的内容直接参考于redis3.0版本源码和《Redis设计与实现》圣经,请大家放心食用~

1.1 SDS(Simple Dynamic String)

1.1.1 概述

Redis的数据类型都是Key-Value键值对,Key永远都是String类型,而我们常说的Redis五大数据类型是指的Value的类型。Redis没用使用传统的C风格字符串作为String的实现,而是自定义了SDS用来作为redis的默认字符串表示。Redis的SDS除了用于String数据的存储之外,还用作缓冲区,如AOF的缓冲区,客户端状态的输入缓冲区等。

1.1.2 底层实现

Redis3.0源码中SDS的实现(sds.h)

/*
 * 类型别名,用于指向 sdshdr 的 buf 属性
 */
typedef char *sds;
/*
 * 保存字符串对象的结构
 */
struct sdshdr {
    // buf 中已占用空间的长度
    int len;

    // buf 中剩余可用空间的长度
    int free;

    // 变长数组,存储数据空间
    char buf[];
};

SDS结构体中的 len + free 的长度是整个SDS字符串的空间大小 - 1,因为在字符串末尾填充了’\0’,这个填充的作用是为了让redis的字符串能兼容部分C语言字符串的API,起到代码重用。

1.1.3 源码实现

Redis的SDS的实现思想其实很类似于Cpp中的vector或者Java中的ArrayList,相信大家一看到这个结构就明白大概是如何进行操作的了。这里就不详细介绍SDS中的所有API,只说一下关键的要点,有需要的朋友可以查看sds.h和sds.c源码文件。

1.不使用结构体指针传递,而使用变长数组传递参数

不过在查阅Redis源码中关于SDS结构体传递有一个注意点。就是所有SDS的传递都是通过直接传递SDS结构体中变长数组buf的地址来传递的(注意SDS结构体定义上方的typedef char *sds)。

那么只通过buf数据的地址如何得知整个结构体的数据呢?

C语言的变长数组的大小是不计入结构体的大小中的,因为数组名实际上不是指针,它就是个地址偏移。并且变长数组的地址是连续衔接在结构体的后方。那么我们使用数组的首地址减去结构体的大小,就得到了结构体的首地址,就可以对结构体数据进行操作了。

 struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
2.底层数组扩容规则

当我们对SDS的字符串进行添加操作的时候,首先会判断当前剩余的长度是否足够,如果足够则不进行扩容,则进行扩容。(对应zmalloc.c文件中的zrealloc函数底层实际上使用realloc实现)

void *zrealloc(void *ptr, size_t size) {
#ifndef HAVE_MALLOC_SIZE
    void *realptr;
#endif
    size_t oldsize;
    void *newptr;

    if (ptr == NULL) return zmalloc(size);
#ifdef HAVE_MALLOC_SIZE
    oldsize = zmalloc_size(ptr);
    newptr = realloc(ptr,size);
    if (!newptr) zmalloc_oom_handler(size);

    update_zmalloc_stat_free(oldsize);
    update_zmalloc_stat_alloc(zmalloc_size(newptr));
    return newptr;
#else
    realptr = (char*)ptr-PREFIX_SIZE;
    oldsize = *((size_t*)realptr);
    newptr = realloc(realptr,size+PREFIX_SIZE);
    if (!newptr) zmalloc_oom_handler(size);

    *((size_t*)newptr) = size;
    update_zmalloc_stat_free(oldsize);
    update_zmalloc_stat_alloc(size);
    return (char*)newptr+PREFIX_SIZE;
#endif
}

扩容规则实际如下

  • 当扩容之后的newlen小于1MB的时候,多分配和newlen大小相同的冗余空间,扩容为 2 * newLen + 1的大小,即SDS的结构体的成员 len == free ==newLen
  • 当扩容之后的newLen大于1MB的时候,则多分配1MB的空间,即扩容为 newLen + 1MB + 1的大小。

1.1.4 使用SDS的好处

  1. 使用O(1)的时间获取字符串长度
    因为SDS结构体中存储了字符串的长度,因此在获取字符串长度的时候无需调用strlen函数,直接就可以获取到。
  2. 防止缓冲区溢出
    传统C语言的字符串拼接函数strcat(dest,source),需要我们程序员保证dest的空间足以容下拼接后的字符串长度,而SDS的free字段记录了当前SDS还有多少可用的空间。如果空间足够则直接拷贝内容,不足则先进行扩容,再执行操作。
  3. 减少字符串修改带来的内存分配次数
    1. 空间预先分配,SDS的空间总是预先分配足够大小的空间,防止String修改频繁申请和释放空间
    2. 惰性空间释放,当程序需要减少SDS的字符串长度的时候,redis并不会直接释放多余的空间,而是使用free字段进行记录,以便下次增加长度时候使用。当然不用担心这部分空间的冗余,如果有需要的话,redis底层会回收这段空间。
  4. SDS是二进制安全
    传统C字符串以’\0’作为字符串的结束标志,但是二进制流等数据中可能就会包含’\0’等特殊字符,使用传统C的字符串会导致数据识别失败。而SDS采用len成员记录的数据的长度,因此可以正确保存图片等二进制数据。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 在 Redis 中,String 是一种基本的数据类型,它可以存储字符串、整数或者浮点数等类型的数据。 String底层实现是一个字节数组,Redis 会根据存储的内容来判断该字符串是一个普通的字符串、整数或者浮点数,并相应地进行编码和存储。 在存储字符串时,Redis 会根据字符串的长度来选择不同的编码方式,可以选择使用普通的字符串编码,也可以使用一种叫做 intset 的紧凑编码方式,intset 可以节省空间,但是仅适用于存储较小的整数。 在存储整数或浮点数时,Redis 会将其转换成二进制格式并存储在字节数组中,这样可以节省存储空间,并且提高访问速度。 除了普通的字符串操作外,Redis 还提供了一些针对 String 类型的特殊操作,如增加或减少一个整数、获取一个子串等。 ### 回答2: Redis 基础类型中的 String 底层实现SDS(Simple Dynamic String)。SDSRedis 自己实现的、用于替代 C 语言中的传统字符串的结构,它在字符串的操作上有很大的优化。 SDS 将字符串的长度和内容分开存储,它的数据结构如下: ``` struct sdshdr { int len; // 字符串长度 int free; // 剩余空间长度 char buf[]; // 字符串内容 }; ``` SDS 的优点主要体现在以下几个方面: 1. 获取字符串长度的时间复杂度为 O(1)。由于 SDS 将长度信息存储在结构体中,我们无需遍历整个字符串才能获取长度,这在某些操作中非常有用。 2. 杜绝缓冲区溢出问题。SDS 会在字符串内容后面留出空间,可以防止字符串内容超出预分配的空间导致的内存溢出问题。 3. 减少字符串扩容时带来的性能开销。当字符串长度超过 SDS 分配的空间时,SDS 会自动进行扩容,而不是每次进行增量扩容,避免了频繁的增加内存的开销。 4. 支持惰性空间释放。当 SDS 进行缩容操作时,不会立即释放多出来的空间,而是将空白部分标记为"free",等待下次进行利用。 总而言之,Redis 中的 String 类型底层实现采用了自己设计的 SDS 结构,它在性能、空间利用和安全性等方面都有很大的优势。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值