SDS
是什么
Simple Dynamic String, 简单动态字符串. 其结构为:
struct SDS {
int free; // 已分配内存中的未使用字节数
int len; // 已分配内存中的已使用字节数, 即字符串的长度
char* buf; // 字节数组
}
buf为一个字节数组, 沿用了C字符串的特征: 以'\0'表示结尾. 这样在buf是一个字符数组时, 可以使用一些c的字符串库.
做什么
在redis中用来表示字符串键. 除了一些只读的字面量使用c字符串表示, 比如一些log打印里面的字面量:
log.error("here is an error!");
其他地方使用到需要修改的字符串对象的地方, 都使用SDS. 如:
set msg "this is a message"
msg是一个字符串对象, 底层实现是SDS; 其值"This is a message"也是一个字符串对象, 底层实现也是SDS.
free属性&内存分配策略
增加内存
发生在对一个SDS进行拓展时, 比如对一个SDS进行append操作时. SDS会采用预分配策略.
当SDS进行拓展操作时, 并不会每次都会去进行申请内存的操作. 而是先看看自己上一次拓展时预分配的内存还够不够用. 如果不够用, 才会真正的申请内存. 除了存储拓展的字符所占的内存空间, 还会额外多申请一部分空间, 这部分的大小为: max(config, 拓展后的字符串长度)
这部分额外内存, 属于已经被分配但是还未被使用的情况. 其大小(字节数)被free属性记录下来.
减少内存
发生在对一个SDS进行缩减时, 比如删除一个SDS中指定的字符.SDS会采用惰性删除策略.
执行删除时, 并不是真正的将这部分内存回收, 还给操作系统. 而是将其使用free属性标记起来, 等到将来拓展时使用; 或者是等待api强制进行内存回收.
总结
SDS使用free属性和len属性, 将操作系统分配给SDS的内存空间分为已使用的和未使用的. 相当于在操作系统的内存分配和SDS的字符存储中间, 建立了一个缓冲区buffer, 这个buffer使用一定的额外内存空间, 降低了操作系统级别的内存分配和内存回收的频率, 从而提高了相关api的性能.
和C字符串比较的优越性
- len属性使得strlen api的复杂度为O(1)
- free属性支持的预分配和惰性删除策略, 通过降低内存分配和回收的频率, 提高了性能, 同时减少了内存碎片的产生.
- api杜绝了缓冲区溢出. 因为SDS已经是一个结构, 而非简单的c字符数组, 持有自己的len和free属性, 因此对自身的内存分配情况了如指掌. 在append操作时会先检查自己可用的内存大小, 在不够时会先进行预分配策略, 从而避免缓冲区溢出.
- 二进制安全. c字符串以及所有c字符串的api都默认使用'\0'作为结尾, 这只能处理一些文本数据, 只能作为纯粹的字符数组使用. 但是一些二进制数据, 比如声音 图像等, 就不能作为字符串进行存储. 而SDS的所有api都是二进制安全的, 其读写也是二进制安全的: 怎么写入的, 读出来的就是什么样的.
链表
是什么
typedef struct ListNode {
struct ListNode * prev;
struct ListNode * next;
void * value;
} ListNode;
typedef struct list {
ListNode * head;
ListNode * tail;
unsigned long len; // 长度
void * (*dup) (void *ptr); // 节点复制函数
void (*free) (void *ptr); // 节点释放函数
int (*match) (void * source, void * target); // 节点比较函数
}
总体而言. 是一个带有头尾指针, 带有len节点计数器, 支持节点类型多态的无环的 双向链表.
做什么
链表提供了顺序访问 以及 高效的增删节点的能力. 并且结构比较简单.
- 列表键的底层实现
- 发布订阅 慢查询 监视器 客户端列表 客户端输出缓冲区