Redis对字符串的特别数据结构设计

  本文目录

简单动态字符串(Simple Dynamic String,SDS)

简单动态字符串SDS的空间分配策略

SDS的空间预分配

空间预分配策略

惰性空间释放

SDS与C语言中字符串的主要区别

SDS如何被存储

        在普通的C、C++或是Java语言中,字符串都是非常常见的数据类型。在Redis中,为了保持性能和Redis通信、分配内存时的安全性,Redis对字符串数据类型做出了特殊的设计。

简单动态字符串(Simple Dynamic String,SDS)

        Redis为了防止字符串的二进制不安全,使用了一个新的结构体结构来表示字符串,以便实现内存安全和降低查询时的时间复杂度。

        首先,我们介绍一下SDS的定义。

        SDS是解决C语言中二进制不安全的缺陷的(Redis使用C语言编写)。在C语言中,字符串采用char[],即char数组进行存储。然而,在C语言中,char数组若存储字符串,必须以" \0 "进行结尾。这造成了字符串长度、位置难以判定,不能存储其他形式的数据(如图像、音频等二进制数据),只能存储文本数据。

        为解决该问题,Redis使用如下的结构体定义:

struct sdshdr{
    int len; // 记录sds字符串在Buf数组中长度,用于定位
    int alloc; // 总共申请了多少空间大小
    char Buf[]; //用于保存字符串
}

        SDS直接可通过len属性判断字符串中" \0 "的位置,保障了二进制安全性。同时,声明了总共申请的空间大小,避免缓冲区溢出。

简单动态字符串SDS的空间分配策略

        在传统C语言中,字符串内部是没有记录长度的,所以,如果强行修改扩充字符串会造成缓冲区溢出的现象(Buffer Overflow)。

        首先,我们来介绍一下到底什么是缓冲区溢出,和它所带来的对程序的不良影响。

        缓冲区溢出是指:计算机向缓冲区内填充数据位数时超过了缓冲区本身的容量,溢出的数据覆盖了原有的合法数据。

        在理想的情况下:一个程序会检查数据长度,而且并不允许输入超过缓冲区长度的字符。

        但是,绝大多数程序都会假设数据长度总是与所分配的储存空间相匹配,这就为缓冲区溢出埋下隐患。操作系统所使用的缓冲区,又被称为“堆栈”,在各个操作进程之间,指令会被临时储存在“堆栈”当中,“堆栈”会出现缓冲区溢出。

         例如,下图指示了内存中的连续空间。该缓冲区空间中存储了两个字符串,分别为“wolf”和“Redis”。在该缓冲区中,只有三个空位可以以供扩容。如果对其中一个字符串进行扩容,例如把“wolf”扩容成“Cutewolf”,则“Redis”的“R”字母将被强行覆盖。

         C语言的上述弊端使得内存泄露的概率大大提升了。所以,SDS采取了空间分配策略,以避免上述情况的出现。

SDS的空间预分配

        空间预分配指的是当我们对SDS进行空间扩展时,如果未使用空间不够用,那么程序不仅为SDS分配所需空间,还会额外分配未使用空间。其中对未使用空间的预分配策略如下:

        1. 若扩大长度后的SDS小于等于1MB,则分配与SDS长度一样大小的未使用空间。

        2. 若扩大长度后的SDS大于1MB,则分配1MB未使用空间大小。

        执行空间预分配策略的好处是:提前分配了未使用空间备用后,就不用每次增长字符串的时候都分配空间,能有效减少内存重新分配的次数。

惰性空间释放

        不仅存在对SDS的扩展操作,同时,SDS也需要减少长度。惰性空间释放指的是在缩小长度时,程序不会立即释放未使用的空间,而是更新 free 属性,即已分配未使用的字符串长度。这样,空间就可以留给下一次使用,避免频繁分配内存磁盘资源造成的浪费。

SDS与C语言中字符串的主要区别

C语言字符串 Redis 中的 SDS字符串 只能保存文本("\0" 必须在末尾)         能保存二进制数据 获取字符串长度的复杂度是O(n):原因为必须遍历字符串才能获得长度 O(1) :原因是长度是SDS数据结构中的一个属性 修改增长字符串可能会造成缓冲区溢出 不会出现缓冲区溢出情况 修改字符串长度 N 次,必然需要N次内存重分配         最多N次,原因是SDS存在预分配策略 可以使用 C 字符串相关的所有函数 可以使用 C 字符串相关的部分函数

SDS如何被存储

SDS是Redis中重要的修饰字符串的数据结构。由于Redis采用字典形式 key-value 来存储数据对象。所以,每次 Redis 都会创建 键对象 与 值对象。而且需要注意的是,在 Redis 中,一个值对象并不是直接存储的,而是被包装成 Redis-Object 对象,并同时将键对象和值对象通过 dictEntry 对象进行封装。SDS作为 dictEntry 中的 Key 的存在形式。例如以下就是一个dictEntry对象,该对象表明了一个 key-value键值对关系:

typedef struct dictEntry {
    void *key;//指向key的指针,key以SDS呈现。
    union {
        void *val;//指向value的指针,void指不知道指针的具体类型
        uint64_t u64;  
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;//指向下一个key-value键值对(哈希值相同的键值对会形成一个链表,从而解决哈希冲突问题)
} dictEntry;

Redis-Object对象的定义如下:

typedef struct redisObject {
    unsigned type:4;//对象类型(占有4位=0.5字节),有五个属性可选择 例如 string list hash set 
    unsigned encoding:4;//编码的具体方式(4位=0.5字节)
    unsigned lru:LRU_BITS;//记录对象最后一次被应用程序访问的时间(24位=3字节)
    int refcount;//引用计数。等于0时表示可以被垃圾回收(32位=4字节)
    void *ptr;//指向底层实际的数据,如一个代表string的robj,它的ptr可能指向一个sds结构;一个代表 
                 //list的robj,它的ptr可能指向一个quicklist。
} robj;

        type属性主要是指对象的具体数据结构属性是什么,如字符串对象、列表对象、哈希对象、集合对象和有序集合对象。

        Encoding属性主要是指的是编码方式是什么,如一个字符串我们可以用整型的ASCII码,或者embstr(长度<44),raw(长度大于44)。

        在Redis中,dictEntry和redisObject的关系如下:

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值