Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。可能几乎所有的线上项目都会使用到 Redis,无论你是做缓存、或是用作消息中间件,用起来很简单方便,但可能大多数人并没有去深入底层的看看 Redis 的一些策略实现等等细节。

正好最近也在项目开发中遇到一些 Redis 相关的 Bug,由于不熟悉底层的一些实现,较为费劲的解决了,所以打算开这么一个系列,记录一下对于 Redis 底层的一些结构、策略的学习笔记。

第一部分我们打算从 Redis 的五种数据结构以及对象类型的实现开始,主要涉及内容如下,你也可以通过文末给出 GitHub 仓库下载对应的思维导图。

本篇文章打算介绍 SDS 简单动态字符串和双端链表这两种数据结构。

一、SDS 简单动态字符串

大家都知道 Redis 是由 C 语言作为底层编程语言实现的,而 C 语言中也是有字符串这种数据结构的,它是一个字符数组并且是一个以空字符结尾的字符数组,这种结构对于 Redis 而言过于简单了,于是 Redis 自行实现了 SDS 这种简单动态字符串结构,它其实和 Java 中 ArrayList 的实现是很类似的。

Redis 源代码中 sds.h 文件下,有五种 sdshdr,它们分别是:

struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* used */
uint8_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len; /* used */
uint16_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len; /* used */
uint32_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; /* used */
uint64_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};

其中,sdshdr5 的注释表明,sdshdr5 is never used。sdshdr5 这种数据结构一般用于存储长度小于 32 个字符的字符串,但现在也已经不再使用这种结构了,再小长度的字符串也建议使用 sdshdr8 进行存储,因为 sdshdr5 少了两个关键字段,因此不具备动态扩容操作,一旦预分配的内存空间使用完,就需要重新分配内存并完成数据的复制迁移,在实际的生产环境中对于性能的影响还是很大的,所以进行了一个抛弃,但其实有些比较小的键依然会采用这种结构存储。

关于 sdshdr5 我们不再多说,我们看其他四种结构的各个字段,len 字段表示当前字符串总长度,也即当前字符串已使用内存大小,alloc 表示为当前字符串分配的总内存大小(不包括len以及flags字段本身分配的内存),因为每一个结构在预分配的时候都会多分配一段内存空间,主要是为了方便以后的扩容。flags 的低三位表示当前 sds 的类型,高五位无用。低三位取值如下:

#define SDS_TYPE_5  0
#define SDS_TYPE_8 1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4

实际上,redis 对 sdshdr 内存分配是禁用内存对齐的,也就是说每个字段分配的内存地址是紧紧排列在一起的, 所以 redis 中字符串参数的传递直接使用 char* 指针。

可能有人会疑问,仅仅通过一个 char 指针如何确定当前字符串的类型,其实由于 sdshdr 内存分配禁止内存对齐,所以 sds[-1] 其实指向的就是 flags 字段的内存地址,通过 flags 字段又可以得到当前 sds 属于哪种类型,进而可以读取头部字段确定 sds 的相关属性。

接下来我们讲讲 sdshdr 相对于传统的 C 语言字符串,性能的提升在哪,以及具有哪些便捷的点。

首先,对于传统的 C 字符串,我想要获取字符串的长度,至少需要 O(n) 遍历一遍数组才行,而我们 sds 只需要 O(1) 的取 len 字段的值即可。

其次,也是非常重要的一个设计,如果我们初始分配了一个字符串对象,那么如果我要在这个字符串后面追加内容的话,限制于数组的长度一经初始化是不能修改的,我们至少需要分配一个足够大的数组,然后将原先的字符串进行一个拷贝。

sdshdr 每次为一个 sds 分配内存的时候都会额外分配一部分暂不使用的内存空间,一般额外的内存会等同于当前字符串占用的内存大小,如果超过 1MB,那么额外空间的内存大小就是 1MB。每当执行 sdscat 这种方法的时候,程序会用 alloc-len 比较下剩下的空余内存是否足够分配追加的内容,如果不够自然触发内存重分配,而如果剩余未使用内存空间足够放下,那么将直接进行分配,无需内存重分配。

通过这种预分配策略, SDS 将连续增长 N 次字符串所需的内存重分配次数从必定 N 次降低为最多 N 次。

最后,对于常规的 C 语言字符串,它通过判断当前字符是否是空字符来决定字符串的结尾,所以就要求你的字符串中不能包含甚至一个空字符,否则空字符后面的字符都不能作为有效字符被读取。而对于某些具有特殊格式要求的,需要使用空字符进行分隔作用的,那么传统的 C 字符串就无法存储了,而我们的 sds 不是通过空字符判断字符串结尾,而是通过 len 字段的值判断字符串的结尾,所以说,sds 还具备二进制安全这个特性,即它可以安全的存储具备特殊格式要求的二进制数据。

关于 sds 我们就简单说到这,它是一种改良版的 C 字符串,兼容 C 语言中既有的函数 API,也通过一些手段提升了某些操作的性能,值得大家借鉴。

二、链表

链表这种数据结构相信大家也不陌生,有很多类型,比如单向链表,双向链表,循环链表等,链表相对于数组来说,一是不需要连续的内存块地址,二是删除和插入的时间复杂度是 O(1) 级别的,非常的高效,但比不上数组的随机访问查询方式。

一样的那句话,没有最好的数据结构,只有恰到好处的数据结构,比如我们后面要介绍的更高层次的数据结构,字典,它的底层其实就依赖的链表规避哈希冲突,具体的我们后面再说。

redis 中借助 C 语言实现了一个双向链表结构:

typedef struct listNode {
struct listNode *prev;
struct listNode *next;
void *value;
} listNode;

pre 指针指向前一个节点,next 指针指向后一个节点,value 指向当前节点对应的数据对象。我盗一张图描述整个串联起来的链表结构:

虽然我通过链表的第一个头节点就可以遍历整个链表,但在 redis 向上封装了一层结构,专门用于表示一个链表结构:

typedef struct list {
listNode *head;
listNode *tail;
void *(*dup)(void *ptr);
void (*free)(void *ptr);
int (*match)(void *ptr, void *key);
unsigned long len;
} list;

head 指向链表的头节点,tail 指向链表的尾节点,dup 函数用于链表转移复制时对节点 value 拷贝的一个实现,一般来说用等于号足以,但某些特殊情况下可能会用到节点转移函数,默认可以给这个函数赋值 NULL 即表示使用等于号进行节点转移。free 函数用于释放一个节点所占用的内存空间,默认赋值 NULL 的话,即使用 redis 自带的 zfree 函数进行内存空间释放,我们也可以来看一下这个 zfree 函数。

void zfree(void *ptr) {
#ifndef HAVE_MALLOC_SIZE
void *realptr;
size_t oldsize;
#endif if (ptr == NULL) return;
#ifdef HAVE_MALLOC_SIZE
update_zmalloc_stat_free(zmalloc_size(ptr));
free(ptr);
#else
realptr = (char*)ptr-PREFIX_SIZE;
oldsize = *((size_t*)realptr);
update_zmalloc_stat_free(oldsize+PREFIX_SIZE);
free(realptr);
#endif
}

这里会涉及到一个内存对齐的概念,就比如一个 64 位的操作系统,一次内存 IO 会固定取出 8 个字节的内存数据出来,如果某个变量横跨了两个八字节段,那么 CPU 需要进行两次的 IO 才能完整取出该变量的数据,引入内存对齐,是为了保证任意变量的内存分配不会出现上述的横跨情况,具体的操作手法就是填充无用的内存位,当然这必然会造成内存碎片,不过这也是一种以空间换时间的策略,你也可以禁用它。

函数的上半部分是做一些判断,如果确定了该指针指向的数据结构占用的总内存,则直接调用 free 函数进行内存的释放,否则需要进行一个计算。redis 中的 zmalloc 在每一次内存数据分配的时候都会追加一个 PREFIX_SIZE 的头部数据块,它的值等于当前系统的最大寻址空间,比如 64 CPU的话,PREFIX_SIZE 就会占用到 8 个字节,并且这 8 个字节内部存储的是当前数据实际占用内存大小。

所以这里的话,ptr 指针向低位移动就是指向头部 PREFIX_SIZE 字段首地址,然后取出里面保存的值,也就是当前数据结构实际占用的内存大小,最后加上它自身传入 update_zmalloc_stat_free 函数中修改 used_memory 内存记录指针的值,并在最后调用 free 函数释放内存,包括头部的部分。

其实我们扯远了,继续看数据结构,这里如果还不是很明白的话,没关系,后面我们还会继续讲的。

match 函数依然是一个多态的实现,只给出了定义,具体实现由你来决定,你也可以选择不实现,它用于比较两个链表节点的 value 值是否相等。返回 0 表示不相等,返回 1 表示相等。

最后一个 len 字段描述的是,整个链表中所包含的节点数量。以上就是 redis 中链表的一个基本的定义,加上 list,最终链表结构在 redis 中呈现的抽象图大概是这样的,依然盗的图:

综上,我们介绍了 redis 中链表的一个基本实现情况,总结一下,它是一个双端链表,也就是查找某个节点的前后节点的时间复杂度都在 O(1),也是一个无环并具有首尾节点指针的链表,初次之外,还具有三个多态函数,用于节点间的复制、比较以及内存释放,需要使用者自行实现。


关注公众不迷路,一个爱分享的程序员。

公众号回复「1024」加作者微信一起探讨学习!

每篇文章用到的所有案例代码素材都会上传我个人 github

https://github.com/SingleYam/overview_java

欢迎来踩!

Redis 的底层数据结构(SDS和链表)的更多相关文章

  1. redis string底层数据结构sds

    redis的string没有采用c语言的字符串数组而采用自定义的数据结构SDS(simple dynamic string)设计 len 为字符串的实际长度  在redis中获取字符串的key长度的时 ...

  2. Redis(二)--- Redis的底层数据结构

    1.Redis的数据结构 Redis 的底层数据结构包含简单的动态字符串(SDS).链表.字典.压缩列表.整数集合等等:五大数据类型(数据对象)都是由一种或几种数结构构成. 在命令行中可以使用 OBJ ...

  3. 深入理解Redis:底层数据结构

    简介 redis[1]是一个key-value存储系统.和Memcached类似,它支持存储的value类型相对更多,包括string(字符串).list(链表).set(集合).zset(sorte ...

  4. Redis详解(四)------ redis的底层数据结构

    上一篇博客我们介绍了 redis的五大数据类型详细用法,但是在 Redis 中,这几种数据类型底层是由什么数据结构构造的呢?本篇博客我们就来详细介绍Redis中五大数据类型的底层实现. 1.演示数据类 ...

  5. Redis 的底层数据结构(对象)

    目前为止,我们介绍了 redis 中非常典型的五种数据结构,从 SDS 到 压缩列表,这都是 redis 最底层.最常用的数据结构,相信你也掌握的不错. 但 redis 实际存储键值对的时候,是基于对 ...

  6. Redis 详解 (四) redis的底层数据结构

    目录 1.演示数据类型的实现 2.简单动态字符串 3.链表 4.字典 5.跳跃表 6.整数集合 7.压缩列表 8.总结 上一篇博客我们介绍了 redis的五大数据类型详细用法,但是在 Redis 中, ...

  7. Redis 的底层数据结构(跳跃表)

    字典相对于数组,链表来说,是一种较高层次的数据结构,像我们的汉语字典一样,可以通过拼音或偏旁唯一确定一个汉字,在程序里我们管每一个映射关系叫做一个键值对,很多个键值对放在一起就构成了我们的字典结构. ...

  8. Redis 的底层数据结构(字典)

    字典相对于数组,链表来说,是一种较高层次的数据结构,像我们的汉语字典一样,可以通过拼音或偏旁唯一确定一个汉字,在程序里我们管每一个映射关系叫做一个键值对,很多个键值对放在一起就构成了我们的字典结构. ...

  9. Redis 的底层数据结构(整数集合)

    当一个集合中只包含整数,并且元素的个数不是很多的话,redis 会用整数集合作为底层存储,它的一个优点就是可以节省很多内存,虽然字典结构的效率很高,但是它的实现结构相对复杂并且会分配较多的内存空间. ...

随机推荐

  1. Viper-Go一站式配置管理工具

    什么是Viper Viper是一个方便Go语言应用程序处理配置信息的库.它可以处理多种格式的配置.它支持的特性: 设置默认值 从JSON.TOML.YAML.HCL和Java properties文件 ...

  2. DMP大数据营销

    一.下载大数据营销APP 使用手机浏览器扫描二维码 二.使用大数据营销APP 1.打开app,如果手机没有打开蓝牙和GPS定位app会自动提示让您打开,若app没有提示请手动去打开蓝牙和GPS 2.搜 ...

  3. 正确使用sqlcipher for Android

    android-database-sqlcipher是基于SQLCipher的数据库加密框架,支持android4到android9,经常用来对android的SqlLite进行加密,现在支持Grad ...

  4. linux安装MySQL后输入mysql显示 ERROR 2002 (HY000): Can't connect to local MySQL server through socket

    我是小白,大佬勿喷 *** linux安装MySQL后输入mysql显示 ERROR 2002 (HY000): Can't connect to local MySQL server through ...

  5. 用gcc/g++编译winsock程序

    用gcc/g++编译winsock程序 D:\My\code>gcc -o getweb.exe getweb.c -lwin32socket 如果不加此句 -lwin32socket 编译会报 ...

  6. Elasticsearch6.x和7.x版本常用插件汇总

    elasticsearch插件汇总 基于es 7.3版本试用. 一.安全插件 1.x-pack a.介绍 包括安全(x-pack-security),监视(x-pack-watcher),警报(x-p ...

  7. linux安装杀软 clamAV

    ClamAV 是Linux平台最受欢迎的杀毒软件,ClamAV 属于免费的开源软件,支持多种平台.ClamAV是基于病毒扫描的命令行工具,但同时也有支持图形界面的ClamTK工具.ClamAV 主要用 ...

  8. ​综述 | SLAM回环检测方法

    本文作者任旭倩,公众号:计算机视觉life成员,由于格式原因,公式显示可能出问题,建议阅读原文链接:综述 | SLAM回环检测方法 在视觉SLAM问题中,位姿的估计往往是一个递推的过程,即由上一帧位姿 ...

  9. UVA 10395 素数筛

    Twin Primes Twin primes are pairs of primes of the form (p; p + 2). The term \twin prime" was c ...

  10. MySQL之主从同步

    一.主从同步概念 1.1 什么是主从同步? MySQL 主从同步是指将数据从一个 MySQL 数据库服务器主节点复制到一个或多个从节点.MySQL 默认采用异步复制方式,这样从节点不用一直访问主服务器 ...