文章目录

  • 前言
  • 1. 动态字符串
    • 1.1 SDS的数据结构
    • 1.2 SDS 与 C 字符串的区别
      • 1.2.1 常数复杂度获取字符串长度
      • 1.2.2 杜绝缓冲区溢出
    • 1.3 减少修改字符串时带来的内存重新分配次数
      • 1.3.1 空间预分配
      • 1.3.2 惰性空间释放
    • 1.4 二进制安全

前言

Redis数据库中的每个键值对都是由对象组成的,其中:

  • 数据库键总是一个字符串对象
  • 数据库键的值则可以是字符串对象、列表对象(list)、哈希对象(hash)、集合对象(set)、有序集合对象(zset)这五种对象中的一种

本篇文章将对以上提到的五种不同类型的对象进行介绍,刨析这些对象所使用的底层数据结构,并说明这些数据结构是如何深刻的影响对象的功能和性能的

1. 动态字符串

Redis没有直接使用C语言传统的字符串表示,而是自己构建了一种名为简单动态字符串的抽象类型,并将SDS用作Redis的默认字符串表示

例如

redis> SET message "hello"
redis> OK

那么Redis将创建一个新的键值对,其中:

  • 键值对的键是一个字符串对象,对象的底层实现是一个保存着字符串"msg"的SDS.
  • 键值对的值也是一个字符串对象,对象的底层实现是一个保存着字符串"hello word” 的 SDS

除了用来保存数据库中的字符串值之外,SDS还被用作缓冲区 ( buffer ):AOF模块中的AOF缓冲区,以及客户端状态中的输入缓冲区,都是由SDS实现的

1.1 SDS的数据结构

struct sdshdr {// 记录 buf 数组中已使用字节的数量,它等于 SDS 所保存字符串的长度int len;// 记录 buf 数组中未使用字节的数量int free;// 字节数组,用于保存字符串char buf[];
};

如下图所示,展示了一个 SDS 示例:

  • free 属性的值为0, 表示这个SDS没有分配任何未使用空间
  • len属性的值为5,表示这个SDS保存了一个五字节长的字符串。
  • buf属性是一个char类型的数组,数组的前五个字节分别保存了’R’、‘e’、‘d’、‘i’、‘s’五个字符,而最后一个字节则保存了空字符’10’。

SDS 遵循C字符串以空字符串结尾的惯例,保存空字符的 1 字节空间不计算在 SDS 的 len 属性里面,并且为空字符分配额外的 1 字节空间

再如下图所示SDS示例:

这个SDS和之前展示的SDS一样,都保存了字符串值"Redis"。这个SDS和之前展示的SDS的区别在于,这个SDS为buf数组分配了五字节未使用空间,所以它的free属性的值为5(图中使用五个空格来表示五字节的未使用空间)

1.2 SDS 与 C 字符串的区别

1.2.1 常数复杂度获取字符串长度

举个例子:想要获取字符串的长度只需访问 SDS 的 len 属性,就可以立即知道 SDS 的长度。

通过使用SDS而不是C字符串,Redis将获取字符串长度所需的复杂度从 O(N) 降低到了 O(1) 这确保了获取字符串长度的工作不会成为Redis的性能瓶颈。

1.2.2 杜绝缓冲区溢出

C字符串除了获取字符串长度的复杂度高之外,它不记录自身长度带来的另一个问题是容易造成缓冲区溢出。

举个例子,假设程序里有两个在内存中紧邻着的C字符串s1和s2,其中s1保存了字符串"Redis",而s⒉则保存了字符串"MongoDB",如图下图所示。

如果将 Cluster拼接到 S1 后,它的内容就为 “Redis Cluster”, 但他却忘了在执行 strcat 之前为 s1 分配足够的空间,那么在 strcat 函数执行之后,s1 的数据将溢出到 s2 所在的空间中,导致 s2 保存的内容被意外地修改,如图下图所示。

这个就是C字符串的缺陷。而SDS的空间分配策略完全杜绝了法生缓冲区溢出的可能性:当对 SDS 进行修改时,SDS API 会先检查空间是否满足修改所需的要求,如果不满足的话,API会自动将 SDS 的空间扩展至执行修改所需的大小,然后才执行实际的修改操作。

1.3 减少修改字符串时带来的内存重新分配次数

因为每次增长或缩短一个 C 字符串,程序都总要对保存这个C字符串的数组进行一次内存分配操作:

  • 如果程序执行的是增长字符串的操作,比如拼接操作( append),那么在执行这个操作之前,程序需要先通过内存重分配来扩展底层数组的空间大小一一如果忘了这一步就会产生缓冲区溢出。
  • 如果程序执行的是缩短字符串的操作,比如截断操作( trim),那么在执行这个操作之后,程序需要通过内存重分配来释放字符串不再使用的那部分空间─-如果忘了这一步就会产生内存泄漏。

因为内存重分配涉及复杂的算法,并且可能需要执行系统调用,所以它通常是一个比较耗时的操作:

  • 在一般程序中,如果修改字符串长度的情况不太常出现,那么每次修改都执行一次内存重分配是可以接受的。
  • 但是Redis作为数据库,经常被用于速度要求严苛、数据被频繁修改的场合,如果每次修改字符串的长度都需要执行一次内存重分配的话,那么光是执行内存重分配的时间就会占去修改字符串所用时间的一大部分,如果这种修改频繁地发生的话,可能还会对性能造成影响。

为了避免C字符串的这种缺陷,SDS 通过未使用空间实现了空间预分配和惰性空间释放两种优化策略

1.3.1 空间预分配

空间与分配用于优化 SDS 的字符串增长操作:当 SDS 的API 对一个 SDS 进行修改,并且需要对 SDS 进行空间扩展的时候,程序不仅会为 SDS 分配修改所需要的空间,还会为 SDS 分配额外的未使用空间。

其中,额外分配的未使用空间数量由以下公式决定:

  • 如果对SDS进行修改之后,SDS的长度(也即是len属性的值)将小于1MB,那么程序分配和len属性同样大小的未使用空间,这时SDS len属性的值将和free属性的值相同。举个例子,如果进行修改之后,SDS 的len将变成13字节,那么程序也会分配13字节的未使用空间,SDS的buf数组的实际长度将变成 13+13+1=27 字节(额外的一字节用于保存空字符).

  • 如果对SDS进行修改之后,SDS 的长度将大于等于1MB,那么程序会分配1MB 的未使用空间。举个例子,如果进行修改之后,SDS的len将变成30MB,那么程序会分配1MB的未使用空间,SDS的buf数组的实际长度为 30 MB + 1 MB + 1bytes

举个例子:
如下图所示,将以下的一个字符串追加 “Cluster”
那么sdscat 将执行一次内存重分配操作,将SDS的长度修改为13字节,并将SDS的未使用空间同样修改为13字节,如图下图所示。

这时在对字符串操作 追加 “Tutorial”
那么这次将不需要执行内存重分配,因为未使用空间里面的13字节足以保存9字节的”Tutorial",追加之后的 SDS 如下图所示。

1.3.2 惰性空间释放

惰性空间释放用于优化SDS的字符串缩短操作:当SDS 的API需要缩短SDS保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待将来使用。

举个例子:

移除如上图所示的 SDS 字符串中的所有 ‘X’ 和 ‘Y’,会将 SDS 修改成如下图所示的样子

注意执行sdstrim之后的SDS并没有释放多出来的8字节空间,而是将这8字节空间作为未使用空间保留在了SDS里面,如果将来要对SDS进行增长操作的话,这些未使用空间就可能会派上用场。

1.4 二进制安全

通过使用二进制安全的SDS,而不是C字符串,使得Redis不仅可以保存文本数据,还可以保存任意格式的二进制数据。

C字符串中的字符必须符合某种编码(比如ASCII),并且除了字符串的末尾之外,字符串里面不能包含空字符,否则最先被程序读入的空字符会被误认为字符串结尾,这些限制使得 C 字符串只能保存文本数据,而不能保存像图片、音频、视频、压缩文件这样的二进制数据。

举个例子: 存入 “Redis Cluster” 这种格式就不能用C字符串来保存,因为C字符串所用的函数只会识别出其中的 “Redis” 而忽略之后的 “Cluster”。

所以为了确保 Redis 可以使用与各种不同的场景,SDS 的 API 都是二进制安全的,所有SDS API 都会14以处理二进制的方式来处理 SDS 存放在 buf 数组里的数据,数据在被写入时是什么样的,它被读取时就是什么样的。

Redis 动态字符串(SDS)底层原理详解相关推荐

  1. 并发编程五:java并发线程池底层原理详解和源码分析

    文章目录 java并发线程池底层原理详解和源码分析 线程和线程池性能对比 Executors创建的三种线程池分析 自定义线程池分析 线程池源码分析 继承关系 ThreadPoolExecutor源码分 ...

  2. 动态代理——拦截器——责任链——AOP面向切面编程底层原理详解(迪丽热巴版)

    目录 动态代理模式详解 前言 什么是代理模式 如何进行代理 静态代理 动态代理 JDK动态代理 CGLIB动态代理 拦截器 责任链模式 博客文章版权申明 动态代理模式详解 前言 代理模式是设计模式中非 ...

  3. Redis源码阅读笔记(1)——简单动态字符串sds实现原理

    首先,sds即simple dynamic string,redis实现这个的时候使用了一个技巧,并且C99将其收录为标准,即柔性数组成员(flexible array member),参考资料见这里 ...

  4. 镜像底层原理详解和基于Docker file创建镜像

    目录 一.镜像底层原理 1.联合文件系统(UnionFS) 2.镜像加载原理 3.为什么Docker里的centos的大小才200M? 二.Dockerfile 1.简介 2.Dockerfile操作 ...

  5. 大三专科实习第一个月——HTTPS底层原理详解

    简介:脱变从现在开始,以上文章讲述的是广度问题接下来方向将是深度的问题.觉得我还可以的可以加群探讨技术QQ群:1076570504 个人学习资料库http://www.aolanghs.com/ 微信 ...

  6. Spring Boot底层原理详解及整合

    Spring Boot框架 通过Spring Boot 可以构建一个基于Spring框架的Java Application,简化配置,自动装配,开箱即用 JavaConfiguration用Java类 ...

  7. 【你好面试官】008 Java内存模型指volatile底层原理详解、多处理器原子操作实现原理

    微信公众号:你好面试官 这里没有碎片化的知识,只有完整的知识体系. 专注于系统全面的知识点讲解,面试题目解析; 如果你觉得文章对你有帮助,欢迎关注.分享.赞赏. ###前言 二蛋几天没有收到面试通知, ...

  8. 大三专科实习第一个月——Socket底层原理详解与应用

    简介:脱变从现在开始,以上文章讲述的是广度问题接下来方向将是深度的问题.觉得我还可以的可以加群探讨技术QQ群:1076570504 个人学习资料库http://www.aolanghs.com/ 微信 ...

  9. java面试题:voliate底层原理——详解

    1. voliate底层原理 1.1 voliate变量的特点 可见性: 当一个线程修改了声明为volatile变量的值,新值对于其他要读该变量的线程来说是立即可见的. 有序性: volatile变量 ...

最新文章

  1. [CF960F]Pathwalks
  2. 【FPGA实现GA】基于FPGA的GA优化算法的设计与实现
  3. Android使用AudioRecord录制pcm音频原始数据以及使用AudioTrack播放
  4. 华为手机连电脑_华为手机微信聊天记录如何导出电脑的四大方法
  5. java_二进制的前导的零
  6. python 删除文件 通配符_python 实现删除文件或文件夹实例详解
  7. Spring注解配置框架
  8. office转PDF文档
  9. 开源.net 混淆器ConfuserEx
  10. 【超详细】私有仓库Gitlab的安装与使用详细教程
  11. 三星 c5 html,三星GALAXY C5/C7参数配置介绍 均支持Samsung Pay
  12. 给想做好新年计划的猿同学
  13. 白杨SEO:流量红利消失,现在都在各渠道做推广,我们还有必要做官方网站吗?怎么做呢?
  14. 梯度下降与随机梯度下降
  15. uipath sequence传递参数_湘西单向滑动球铰支座设计参数深化,期待合作
  16. HTML中gt的含义
  17. vue项目json格式化显示
  18. c++二叉排序树的非递归插入与递归插入,递归之间不同写法的思考
  19. 体检预约系统项目总结
  20. 算法分析与设计第五章作业

热门文章

  1. 在vue项目中快速使用element UI
  2. Ubuntu和Windows相互共享文件夹
  3. No enclosing instance of type FormDetailBean is accessible. Must qualify the allocation with an encl
  4. 点云obb盒显示 python
  5. [散分] 眼见为实?_眼见为实
  6. c#调用企业微信服务端API发送消息和图片
  7. 生物在计算机应用,生物技术在计算机领域中的应用有哪些
  8. Kappa(cappa)系数只需要看这一篇就够了,算法到python实现
  9. 淘宝能承受几百上亿的访问点击,而铁道部的网站为啥分分钟崩溃?
  10. c语言编程图案大全,C语言中图案的编程