前言
我们都知道,redis最基本的数据结构有5种,分别是字符串、列表、哈希表、集合和有序集合。其实准确来说,这种表述容易造成误会,给人误解。从redis的源码来看,这5种其实是redis封装的对象,而底层对象的实现才应该成为数据结构。redis最基础的数据结构包括以下几种:简单动态字符串、链表、字典、跳跃表、整数集合、压缩列表。而redis对象(也就是一开始提到的5种)其实底层都是基于这些数据结构的,这样的好处是,同一种对象根据实际场景的不同底层可以使用不同的数据机构来优化使用效率。redis的对象还基于引用计数实现了内存回收机制和对象共享机制,这里就不展开了。本文参考《Redis 设计与实现》主要介绍一下redis的6种底层数据结构。
简单动态字符串
定义
Redis 没有直接使用 C 语言传统的字符串表示(以空字符结尾的字符数组,以下简称 C 字符串), 而是自己构建了一种名为简单动态字符串(simple dynamic string,SDS)的抽象类型, 并将 SDS 用作 Redis 的默认字符串表示。如果 Redis 需要一个可以改变的字符串对象的时候,底层就会使用 SDS 来实现,SDS 结构的源码实现如下:
/*
* 保存字符串对象的结构
*/
struct sdshdr {
// buf 中已占用空间的长度
int len;
// buf 中剩余可用空间的长度
int free;
// 数据空间
char buf[];
};
buf作为字节数组,底层保留了C语言以空字符('0')结尾的特性以便重用部分C的函数,空字符不计算长度。
与C语言字符串的区别
Redis 不直接使用 C 的字符串肯定是因为有自己的考虑,就是因为 SDS 这种实现相比原生的 C 字符串有很多优点。
- 常数复杂度获取字符串长度
由 SDS 的源码可以看到,获取长度直接返回len即可,而 C 的字符串需要遍历,时间复杂度为O(N)。
- 杜绝缓冲区溢出
C 语言进行字符串拼接时需要人工考虑内存的分配,否则可能导致内存溢出从而覆盖已有的数据。而 SDS 在拼接时直接查询 free 字段即可知道空间是否足够,不够会自动进行扩展。
- 减少修改字符串时带来的内存重分配次数
C 字符串以空字符结尾,长度固定为 (有效字符数 + 1),每次进行字符串拼接或者截断操作都需要进行内存重分配,否则会造成内存溢出和内存泄露。SDS 通过空间预分配和惰性空间释放来解决这个问题,free 字段记录了有效字符空间与实际数组空间的差值,SDS 扩展时会分配额外空间,这称为空间预分配;SDS 缩短时不会立即进行空间释放(除非手动调用API进行空间释放),会用free字段记录释放的空间,这称为惰性空间释放。
- 二进制安全
C 的字符串不能保存含有空字符的数据,因为空字符是用来判断字符串结尾的,所以不能用于保存二进制数据。SDS 用len判断字符串长度可以保存任意二进制数据。
- SDS 可以兼容部分 C 字符串函数
因为buf作为字符数组也是用空字符结尾的,所以部分 C 语言的函数是可以直接使用的
C 字符串 | SDS |
---|---|
获取字符串长度的复杂度为 O(N) 。 | 获取字符串长度的复杂度为 O(1) 。 |
API 是不安全的,可能会造成缓冲区溢出。 | API 是安全的,不会造成缓冲区溢出。 |
修改字符串长度 N 次必然需要执行 N 次内存重分配。 | 修改字符串长度 N 次最多需要执行 N 次内存重分配。 |
只能保存文本数据。 | 可以保存文本或者二进制数据。 |
可以使用所有 库中的函数。 | 可以使用一部分 库中的函数。 |
链表
实现
Redis 实现的链表是一个双向链表,每个节点有特定实现