当前位置: 首页 > news >正文

Redis核心技术与实战学习笔记

Redis核心技术与实战学习笔记

  • 最近想沉下心来看下redis,买了蒋德钧老师的《Redis 核心技术与实战》,这里记录一些学习笔记 希望能够坚持下去
  • 有想一起学习的童鞋,可以点击跳转到文章尾部获取学习资源,仅供学习不要用于任何商业用途!!!

redis知识全景图

  • 学习要有一个全局的系统观念

  • 在这里插入图片描述

redis问题画像图

  • redis问题可以多套到这个图中分析形成统一的印象
  • 问题 --> 主线 --> 技术点
  • 在这里插入图片描述

SimpleKV需要考虑的问题

  • 存什么样的数据

    • KV对 key为string,value为基本类型
  • 数据有哪些操作

    • 增删改查 PUT/GET/DEL/Scan
    • 不论是增删改查都涉及到索引问题,因为要先定位到key对应的内存位置再读写 这里有涉及到索引 可以考虑全局hash
      • 这里如果value是复杂结构 hash后还要再次用到不同的索引算法
    • 如果是增和删除 又涉及到内存管理 因为需要分配内存或者释放内存,如果value大小不一致 内存分配算法至关重要
  • 数据存在哪里

    • 存在内存比较快 但内存没了容易丢失,所以还要解决持久化的问题,每次都落地则性能不行,可定时落地到文件
  • 客户端怎么访问

    • .so动态连接库 单机访问
    • socket连接 可以跨机器 但要解决连接管理,协议解析
    • 多个客户端并发访问 如何高性能 涉及到IO模型
  • 容灾怎么来做

    • 涉及到主从或者集群
  • 重启后怎么快速初始化或者恢复

    • 也是涉及到持久化

    • 在这里插入图片描述

redis数据结构

值的数据类型

  • String、List、Hash、Set、sortedSet

KV保存用到的数据结构

整体图示

  • 在这里插入图片描述

  • 在 Redis 3.0 版本中 List 对象的底层数据结构由「双向链表」或「压缩表列表」实现,但是在 3.2 版本之后,List 数据类型底层数据结构是由 quicklist 实现的;

  • 在最新的 Redis 代码(还未发布正式版本)中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了

全局Hash索引示意

  • 在这里插入图片描述

hash冲突解决办法

  • 链式hash 冲突的时候用链表的方式保存冲突的数据,所以多个冲突的数据要遍历就要逐个遍历了

  • 在这里插入图片描述

  • 2个全局hash,渐进式rehash

    • 直接全量rehash涉及大量内存复制操作,可能阻塞客户端请求处理,所以采取的是渐进式rehash

    • 在这里插入图片描述

redis数据结构全景图_xiaolin

  • 在这里插入图片描述

  • redisDb 结构,表示 Redis 数据库的结构,结构体里存放了指向了 dict 结构的指针;

  • dict 结构,结构体里存放了 2 个哈希表,正常情况下都是用「哈希表1」,「哈希表2」只有在 rehash 的时候才用,具体什么是 rehash,我在本文的哈希表数据结构会讲;

  • ditctht 结构,表示哈希表的结构,结构里存放了哈希表数组,数组中的每个元素都是指向一个哈希表节点结构(dictEntry)的指针;

  • dictEntry 结构,表示哈希表节点的结构,结构里存放了 void * key 和 void * value 指针, *key 指向的是 String 对象,而 *value 则可以指向 String 对象,也可以指向集合类型的对象,比如 List 对象、Hash 对象、Set 对象和 Zset 对象

  • void * key 和 void * value 指针指向的是 Redis 对象

redisObject数据结构示意
  • 在这里插入图片描述

  • type,标识该对象是什么类型的对象(String 对象、 List 对象、Hash 对象、Set 对象和 Zset 对象);

  • encoding,标识该对象使用了哪种底层的数据结构;

  • ptr,指向底层数据结构的指针

简单字符串SDS(simple dynamic string)

SDS数据结构

struct SDS{len   //这样获取字符串长度的时候,只需要返回这个成员变量值就行,时间复杂度只需要 O(1)alloc //分配空间长度 类似capicity 分配给字符数组的空间长度。这样在修改字符串的时候,可以通过 alloc - len 计算出剩余的空间大小,可以用来判断空间是否满足修改需求,如果不满足的话,就会自动将 SDS 的空间				扩展至执行修改所需的大小,然后才执行实际的修改操作,所以使用 SDS 既不需要手动修改 SDS 的空间大小,也不会出现前面所说				的缓冲区溢出的问题当判断出缓冲区大小不够用时,Redis 会自动将扩大 SDS 的空间大小(小于 1MB 翻倍扩容,大于 1MB 按 1MB 扩容flag  //用来表示不同类型的 SDS。一共设计了 5 种类型,分别是 sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64每种类型占用内存不一样 更加灵活 而且是禁止内存字节对齐的,节省内存buf[] 字符数组,用来保存实际数据。不仅可以保存字符串,也可以保存二进制数据  
}

链表List

typedef struct listNode {//前置节点struct listNode *prev;//后置节点struct listNode *next;//节点的值void *value;
} listNode;
在这个基础上增加
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;
  • 在这里插入图片描述

Redis 的链表实现优点如下:

  • listNode 链表节点的结构里带有 prev 和 next 指针,获取某个节点的前置节点或后置节点的时间复杂度只需O(1),而且这两个指针都可以指向 NULL,所以链表是无环链表
  • list 结构因为提供了表头指针 head 和表尾节点 tail,所以获取链表的表头节点和表尾节点的时间复杂度只需O(1)
  • list 结构因为提供了链表节点数量 len,所以获取链表中的节点数量的时间复杂度只需O(1)
  • listNode 链表节使用 void* 指针保存节点值,并且可以通过 list 结构的 dup、free、match 函数指针为节点设置该节点类型特定的函数,因此链表节点可以保存各种不同类型的值

链表的缺陷也是有的:

  • 链表每个节点之间的内存都是不连续的,意味着无法很好利用 CPU 缓存。能很好利用 CPU 缓存的数据结构就是数组,因为数组的内存是连续的,这样就可以充分利用 CPU 缓存来加速访问。
  • 还有一点,保存一个链表节点的值都需要一个链表节点结构头的分配,内存开销较大

因此,Redis 3.0 的 List 对象在数据量比较少的情况下,会采用「压缩列表」作为底层数据结构的实现,它的优势是节省内存空间,并且是内存紧凑型的数据结构。

不过,压缩列表存在性能问题(具体什么问题,下面会说),所以 Redis 在 3.2 版本设计了新的数据结构 quicklist,并将 List 对象的底层数据结构改由 quicklist 实现。

然后在 Redis 5.0 设计了新的数据结构 listpack,沿用了压缩列表紧凑型的内存布局,最终在最新的 Redis 版本,将 Hash 对象和 Zset 对象的底层数据结构实现之一的压缩列表,替换成由 listpack 实现。

压缩列表

压缩列表的最大特点,就是它被设计成一种内存紧凑型的数据结构,占用一块连续的内存空间,不仅可以利用 CPU 缓存,而且会针对不同长度的数据,进行相应编码,这种方法可以有效地节省内存开销。

但是,压缩列表的缺陷也是有的:

  • 不能保存过多的元素,否则查询效率就会降低;
  • 新增或修改某个元素时,压缩列表占用的内存空间需要重新分配,甚至可能引发连锁更新的问题。

因此,Redis 对象(List 对象、Hash 对象、Zset 对象)包含的元素数量较少,或者元素值不大的情况才会使用压缩列表作为底层数据结构。

  • 在这里插入图片描述

  • *zlbytes*,记录整个压缩列表占用对内存字节数;

  • *zltail*,记录压缩列表「尾部」节点距离起始地址由多少字节,也就是列表尾的偏移量;

  • *zllen*,记录压缩列表包含的节点数量;

  • *zlend*,标记压缩列表的结束点,固定值 0xFF(十进制255)。

  • *prevlen*,记录了「前一个节点」的长度;

  • *encoding*,记录了当前节点实际数据的类型以及长度;

  • *data*,记录了当前节点的实际数据;

    当我们往压缩列表中插入数据时,压缩列表就会根据数据是字符串还是整数,以及数据的大小,会使用不同空间大小的 prevlen 和 encoding 这两个元素里保存的信息,这种根据数据大小和类型进行不同的空间大小分配的设计思想,正是 Redis 为了节省内存而采用的

    分别说下,prevlen 和 encoding 是如何根据数据的大小和类型来进行不同的空间大小分配。

    压缩列表里的每个节点中的 prevlen 属性都记录了「前一个节点的长度」,而且 prevlen 属性的空间大小跟前一个节点长度值有关,比如:

    • 如果前一个节点的长度小于 254 字节,那么 prevlen 属性需要用 1 字节的空间来保存这个长度值;
    • 如果前一个节点的长度大于等于 254 字节,那么 prevlen 属性需要用 5 字节的空间来保存这个长度值;

    encoding 属性的空间大小跟数据是字符串还是整数,以及字符串的长度有关:

    • 如果当前节点的数据是整数,则 encoding 会使用 1 字节的空间进行编码。
    • 如果当前节点的数据是字符串,根据字符串的长度大小,encoding 会使用 1 字节/2字节/5字节的空间进行编码。
    连锁更新

    压缩列表除了查找复杂度高的问题,还有一个问题。

    压缩列表新增某个元素或修改某个元素时,如果空间不不够,压缩列表占用的内存空间就需要重新分配。而当新插入的元素较大时,可能会导致后续元素的 prevlen 占用空间都发生变化,从而引起「连锁更新」问题,导致每个元素的空间都要重新分配,造成访问压缩列表性能的下降

    前面提到,压缩列表节点的 prevlen 属性会根据前一个节点的长度进行不同的空间大小分配:

    • 如果前一个节点的长度小于 254 字节,那么 prevlen 属性需要用 1 字节的空间来保存这个长度值;
    • 如果前一个节点的长度大于等于 254 字节,那么 prevlen 属性需要用 5 字节的空间来保存这个长度值;

    现在假设一个压缩列表中有多个连续的、长度在 250~253 之间的节点

    ​ 因为这些节点长度值小于 254 字节,所以 prevlen 属性需要用 1 字节的空间来保存这个长度值

    这时,如果将一个长度大于等于 254 字节的新节点加入到压缩列表的表头节点,即新节点将成为 e1 的前置节点

    • 在这里插入图片描述

e1 原本的长度在 250~253 之间,因为刚才的扩展空间,此时 e1 的长度就大于等于 254 了,因此原本 e2 保存 e1 的 prevlen 属性也必须从 1 字节扩展至 5 字节大小。

正如扩展 e1 引发了对 e2 扩展一样,扩展 e2 也会引发对 e3 的扩展,而扩展 e3 又会引发对 e4 的扩展…. 一直持续到结尾。

这种在特殊情况下产生的连续多次空间扩展操作就叫做「连锁更新」,就像多米诺牌的效应一样,第一张牌倒下了,推动了第二张牌倒下;第二张牌倒下,又推动了第三张牌倒下….,

压缩列表的缺陷

空间扩展操作也就是重新分配内存,因此连锁更新一旦发生,就会导致压缩列表占用的内存空间要多次重新分配,这就会直接影响到压缩列表的访问性能

所以说,虽然压缩列表紧凑型的内存布局能节省内存开销,但是如果保存的元素数量增加了,或是元素变大了,会导致内存重新分配,最糟糕的是会有「连锁更新」的问题

因此,压缩列表只会用于保存的节点数量不多的场景,只要节点数量足够小,即使发生连锁更新,也是能接受的。

虽说如此,Redis 针对压缩列表在设计上的不足,在后来的版本中,新增设计了两种数据结构:quicklist(Redis 3.2 引入) 和 listpack(Redis 5.0 引入)。这两种数据结构的设计目标,就是尽可能地保持压缩列表节省内存的优势,同时解决压缩列表的「连锁更新」的问题。

仅供学习 切记用于任何商业用途

关注 _微_信_公_众_号 疯子爱淡定 回复 Redis核心技术学习

  • 在这里插入图片描述

Hash

typedef struct dict {…//两个Hash表,交替使用,用于rehash操作dictht ht[2]; …
} dict;
---------------------------->
typedef struct dictht {//哈希表数组dictEntry **table;//哈希表大小unsigned long size;  //哈希表大小掩码,用于计算索引值unsigned long sizemask;//该哈希表已有的节点数量unsigned long used;
} dictht;
---------------------------->
typedef struct dictEntry {//键值对中的键void *key;//键值对中的值union {void *val;uint64_t u64;int64_t s64;double d;} v;//指向下一个哈希表节点,形成链表struct dictEntry *next;
} dictEntry;
负载因子=已保存节点数量/hash表大小
  • 当负载因子大于等于 1 ,并且 Redis 没有在执行 bgsave 命令或者 bgrewiteaof 命令,也就是没有执行 RDB 快照或没有进行 AOF 重写的时候,就会进行 rehash 操作。
  • 当负载因子大于等于 5 时,此时说明哈希冲突非常严重了,不管有没有有在执行 RDB 快照或 AOF 重写,都会强制进行 rehash 操作

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 追问试面试系列:线程池
  • 【区块链+绿色低碳】泸州:“绿芽积分”号召全民绿色减碳 | FISCO BCOS应用案例
  • 前端构建工具Vite
  • 【React】useState:状态更新规则详解
  • Linux运维高频用途脚本和命令行,持续更新......
  • Hutool中SpringUtil中的一个坑-所有路由消失
  • docker安装elasticsearch(es)最新版本
  • 3.5.3、查找和排序算法-插入类排序和选择类排序
  • Dify中语音和文字间转换问题的一种暂时注释方式
  • 大模型算法面试题(十七)
  • Django实战:开启数字化任务管理的新纪元
  • C/C++编程-算法学习-数字滤波器
  • 日常踩坑---ljmp在BIOS中的使用
  • pytest-html报告修改与汉化
  • CTF-NSSCTF[NISACTF 2022]
  • 「前端早读君006」移动开发必备:那些玩转H5的小技巧
  • express.js的介绍及使用
  • extract-text-webpack-plugin用法
  • JavaScript 是如何工作的:WebRTC 和对等网络的机制!
  • Laravel 实践之路: 数据库迁移与数据填充
  • MYSQL 的 IF 函数
  • Mysql数据库的条件查询语句
  • Phpstorm怎样批量删除空行?
  • 编写高质量JavaScript代码之并发
  • - 概述 - 《设计模式(极简c++版)》
  • 关于extract.autodesk.io的一些说明
  • 简单数学运算程序(不定期更新)
  • 可能是历史上最全的CC0版权可以免费商用的图片网站
  • 区块链技术特点之去中心化特性
  • 实战|智能家居行业移动应用性能分析
  • 使用docker-compose进行多节点部署
  • 网页视频流m3u8/ts视频下载
  • 移动端 h5开发相关内容总结(三)
  • 正则表达式
  • ​比特币大跌的 2 个原因
  • # 达梦数据库知识点
  • #Java第九次作业--输入输出流和文件操作
  • #我与Java虚拟机的故事#连载15:完整阅读的第一本技术书籍
  • ()、[]、{}、(())、[[]]等各种括号的使用
  • (02)Hive SQL编译成MapReduce任务的过程
  • (32位汇编 五)mov/add/sub/and/or/xor/not
  • (aiohttp-asyncio-FFmpeg-Docker-SRS)实现异步摄像头转码服务器
  • (java)关于Thread的挂起和恢复
  • (附源码)spring boot校园拼车微信小程序 毕业设计 091617
  • (附源码)计算机毕业设计ssm本地美食推荐平台
  • (附源码)计算机毕业设计ssm基于Internet快递柜管理系统
  • (淘宝无限适配)手机端rem布局详解(转载非原创)
  • (原創) 人會胖會瘦,都是自我要求的結果 (日記)
  • (转)重识new
  • ***检测工具之RKHunter AIDE
  • .360、.halo勒索病毒的最新威胁:如何恢复您的数据?
  • .NET Core使用NPOI导出复杂,美观的Excel详解
  • .NET/C# 推荐一个我设计的缓存类型(适合缓存反射等耗性能的操作,附用法)
  • .net用HTML开发怎么调试,如何使用ASP.NET MVC在调试中查看控制器生成的html?
  • .NET中使用Protobuffer 实现序列化和反序列化