SDS是Redis中实现的一种数据结构,用来存储字符串,代码实现以下:算法
// 文件路径:src/sds.h struct sdshdr { // 记录buf数组中已使用字节的数量 // 等于SDS所保存字符串的长度 int len; // 记录buf数组中未使用字节的数量 int free; // 字节数组,用于保存字符串 char buf[]; };
常规C字符串并不记录自身的长度信息,因此为了获取一个C字符串的长度,程序必须遍历整个字符串,对遇到的每一个字符进行计数,知道遇到表明字符串结尾的空字符为止,这个操做的复杂度为O(N)。数据库
而SDS使用结构体实现,结构体中的len属性直接记录了该SDS结构体中buf数组中已使用的长度,所以获取字符串长度时,只须要获取len属性的值,这个操做的复杂度为O(1)。数组
SDS结构体的实现确保了获取字符串长度的工做不会成为Redis的性能瓶颈。安全
由于C字符串不记录自身的长度,因此当进行字符串复制的时候,若是分配内存不够,便有可能产生缓冲区溢出。数据结构
而在Redis中,当SDS API须要对SDS进行修改时,API会先检查SDS的空间是否知足修改所需的要求,若是不知足的话,API会自动将SDS的空间扩展至执行修改所需的大小,而后才执行实际的修改操做,因此使用SDS既不须要手动修改SDS的空间大小,也不会出现前面所说的缓冲区溢出问题。curl
Redis中 strcat
的实现代码:函数
sds sdscat(sds s, const char *t) { return sdscatlen(s, t, strlen(t)); }
Redis中 sdscatlen
的实现代码:性能
sds sdscatlen(sds s, const void *t, size_t len) { struct sdshdr *sh; // 原有字符串长度 直接获取len属性 size_t curlen = sdslen(s); // 扩展 sds 空间 // T = O(N) s = sdsMakeRoomFor(s,len); // 内存不足?直接返回 if (s == NULL) return NULL; // 复制 t 中的内容到字符串后部 // T = O(N) sh = (void*) (s-(sizeof(struct sdshdr))); memcpy(s+curlen, t, len); // 更新属性 sh->len = curlen+len; sh->free = sh->free-len; // 添加新结尾符号 s[curlen+len] = '\0'; // 返回新 sds return s; }
常规C字符串,再执行拼接操做或者截断操做时,一般会对数组进行内存重分配,而内存重分配操做涉及复杂的算法,而且可能执行系统调用,因此它一般是一个比较耗时的操做。优化
Redis做为数据库,会对数据进行频繁的修改,而且对速度要求极为严苛,因此每次修改字符串长度都须要进行内存重分配会对性能形成极大的影响。url
所以,SDS实现了空间预分配和惰性空间释放两种优化策略。
当SDS的API对一个SDS进行修改时,程序不只会为SDS分配必需要的空间,还会为SDS分配额外的未使用空间。
额外分配未使用空间数量的规则:
当SDS的len属性值小于1MB,程序分配和len属性一样大小的未使用空间。
当SDS的len属性值大于1MB,程序将多分配1M的未使用空间。
经过这种预分配策略,SDS将连续增加N次字符串所需的内存重分配次数从一定N次下降为最多N次。
当对SDS进行字符串缩短操做时,SDS的API不会当即使用内存重分配回收多出来的字节,而是使用free属性将这些字节的数量记录起来,等待未来使用。
固然,SDS也提供了相应的API,能够用来真正释放SDS的未使用空间,因此不用担忧惰性空间释放策略会形成内存浪费。
C字符串和SDS之间的区别
C字符串 | SDS |
---|---|
获取字符串长度的复杂度为O(N) | 获取字符串长度的复杂度为O(1) |
API是不安全的,可能形成缓冲区溢出 | API是安全的,不会形成缓冲区溢出 |
修改字符串长度N次必然须要执行N次内存重分配 | 修改字符串长度N次最多需啊哟执行N次内存重分配 |
可使用全部<string.h>库中的函数 | 可使用一部分<string.h>库中的函数 |
感谢huangz的《Redis设计与实现》,本文纯属读书笔记,资源均来源于书中。
原文做者:我才是二亮
原文连接:http://www.2liang.me/archives/264转载必须在正文中标注并保留原文连接、做者等信息。