本文目录
简单动态字符串(Simple Dynamic String,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的关系如下: