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

Redis采用SDS(Simple Dynamic String)作为默认字符串实现,而非C风格字符串。SDS提供O(1)时间获取长度,防止缓冲区溢出,减少内存分配次数,并且是二进制安全的。其扩容规则包括当字符串增长小于1MB时分配两倍空间,大于1MB时额外分配1MB。SDS的好处包括快速获取长度、避免溢出、优化内存管理和支持二进制数据存储。
摘要由CSDN通过智能技术生成

本系列所有的内容直接参考于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成员记录的数据的长度,因此可以正确保存图片等二进制数据。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值