Redis 内存压缩原理

Redis 无疑是一个大量消耗内存的数据库,因此 Redis 引入了一些设计巧妙的数据结构进行内存压缩来减轻负担。ziplist、quicklist 以及 intset 是其中最常用最重要的压缩存储结构。

Redis对外提供了 string, list, hash, set, zset等数据类型,每种数据类型可能存在多种不同的底层实现,这些底层数据结构被称为编码(encoding)。

以 list 类型为例,其经典的实现方式为双向链表(linkedlist)。双向链表的每个节点拥有一个前向指针一个后向指针,在64位系统下每个节点占用了 2 * 64bit = 16 Byte 的额外空间。因此当 list 中元素较少时会使用 ziplist 作为底层数据结构。

object encoding <key></key> 命令可以查看某个 key 的编码类型:

127.0.0.1:6379> set a 1
OK
127.0.0.1:6379> object encoding a
"int"
127.0.0.1:6379> rpush l 1
(integer) 1
127.0.0.1:6379> object encoding l
"ziplist"

先总结一下各种数据结构可以使用的编码类型,下文再对这些压缩类型进行详细说明:

  • string
  • raw: 动态字符串(SDS)
  • embstr: 优化内存分配的字符串编码
  • int: 整数
  • list
  • linkedlist
  • ziplist
  • quicklist
  • set
  • hashtable
  • intset
  • hash
  • ziplist
  • hashtable
  • zset(sortedset)
  • ziplist
  • skiplist

本文接下来将详细说明各种压缩编码的原理以及编码决定规则。

ziplist 是一段连续内存,类似于数组结构。当元素比较少时使用数组结构不仅节省内存,而且遍历操作的开销也不大。因此 list, hash, zset 在元素较少时都采用 ziplist 存储。

ziplist 存储为一段裸二进制数据(unsigned char *), 可以看到源代码中大量使用宏进行定义,虽然节省了大量内存但是代码可读性较低。

ziplist 的结构:

<zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend>
</zlend></entry></entry></entry></zllen></zltail></zlbytes>
  • zlbytes: uint32 型, 存储整个ziplist当前被分配的空间,包含自身占用的4个字节。
  • zltail: uint32 型, 存储ziplist中最后一个entry相对头部的偏移量, 用于直接访问尾端元素避免遍历。
  • zllen: uint16 型, 记录 ziplist 中元素的个数
  • entry: 实际存储元素的单元
  • zlend: 魔法数字 255 标记 ziplist 的结尾, 没有 entry 以 0xff 开头不会出现误判的问题

entry 是实际存储数据的单元, 可以存储 int 或 string 类型数据。在存储 string 类型数据时 entry 的结构为:

  • prevlen: 表示前一个 entry 的长度,用于从后向前遍历。
  • encoding: 存储当前 entry 的数据类型和长度
  • entry-data: 实际的数据部分

当存储 int 类型的数据时, 数据(entry-data)会被合并到 encoding 内部,此时没有 entry-data 字段。

当前一个元素长度小于254(255用于zlend)时,prevlen长度为1个字节,值为前一个entry的长度;如果长度大于等于254,prevlen 用5个字节表示,第一字节设置为254,后面4个字节存储一个小端的无符号整型,表示前一个entry的长度。

encoding 用来表示 entry 的数据类型和长度。encoding 的全部定义可以在 ziplist.c 中找到。

下面列出几种 encoding 的示例,encoding 中的字母表示一个bit:

  • 00pppppp: encoding 的长度为一个字节,后6位表示字符串的长度。因为长度最多6位,因此字符串的长度不超过63
  • 01pppppp qqqqqqqq: encoding 的长度为两个字节, 后14位存储字符串的长度,因此字符串的长度不超过16383
  • 11000000: encoding为3个字节,后2个字节表示一个int16
  • 1110000: encoding为4个字节,后3个字节表示一个有符号整型
  • 11111111: zlend

前面提到每个 entry 都会有一个 prevlen 字段存储前一个 entry 的长度。如果内容小于 254 字节,prevlen 用 1 字节存储,否则就是 5 字节。这意味着如果某个 entry 经过了修改操作从 253 字节变成了 254 字节,那么它的下一个 entry 的 prevlen 字段就要更新,从 1 个字节扩展到 5 个字节;如果这个 entry 的长度本来也是 253 字节,那么后面 entry 的 prevlen 字段还得继续更新。这种现象被称为 ziplist 的 级联更新,添加、修改、删除元素的操作都有可能导致级联更新。

ziplist 不会预留扩展空间,每次插入一个新的元素就需要调用 realloc 扩展内存, 并可能需要将原有内容拷贝到新地址。

综上,ziplist 是一个使用连续内存存储数据,类似于数组的数据结构。可以 O(1) 的时间复杂度访问首尾元素。因为 entry 长度不确定,可以向前或向后顺序访问,不能随机访问。因为级联更新的现象的存在,添加、修改、删除元素操作的复杂度在 O(n) 到 O(n^2) 之间。

在满足下列条件时, list, hash 和 sortedset 三种结构会采用 ziplist 编码:

  • list: value 字节数

ziplist 存储 list 时每个元素会作为一个 entry; 存储 hash 时 key 和 value 会作为相邻的两个 entry; 存储 zset 时 member 和 score 会作为相邻的两个entry。

当不满足上述条件时,ziplist 会升级为 linkedlist, hashtable 或 skiplist 编码。在任何情况下大内存的编码都不会降级为 ziplist。

Redis 3.2 版本引入了 quicklist 作为 list 的底层实现,不再使用 linkedlist 和 ziplist 实现。 quicklist 是 ziplist 组成的双向链表,它的每个节点都是一个 ziplist。

quicklist 是结合了 linkedlist 和 ziplist 优点的产物:

  • linkedlist 便于进行增删改操作但是内存占用较大
  • ziplist 内存占用较少,但是因为每次修改都可能触发 realloc 和 memcopy, 并且可能导致级联更新。因此修改操作的效率较低,在 ziplist 较长时这个问题更加突出。

于是每个节点上 ziplist 的大小变成了一个需要折中的难题:

  • ziplist 越小,quicklist 越接近于 linkedlist。此时存储效率下降,但是修改操作的效率较高。
  • ziplist 越大,quicklist 越接近于 ziplist。此时存储效率上升,但是修改操作的效率降低。

redis 根据 list-max-ziplist-size 配置项来决定节点上 ziplist 的长度。

list-max-ziplist-size 为正值的时候,表示按照数据项个数来限定每个 quicklist 节点上的 ziplist 长度。比如,当这个参数配置成5的时候,表示每个 quicklist 节点的ziplist 最多包含5个数据项。

当为负值的时候,表示按照占用字节数来限定每个节点上的 ziplist 长度。这时,它只能取 -1 到 -5 这五个值:

  • -5: 每个节点上的 ziplist 大小不能超过64 KB
  • -4: 每个节点上的 ziplist 大小不能超过 32 KB。
  • -3: 每个节点上的 ziplist 大小不能超过16 Kb。
  • -2: 每个节点上的 ziplist 大小不能超过8 Kb。这是 redis 的默认设置。
  • -1: 每个节点上的 ziplist 大小不能超过4 Kb。

压缩中间节点

对于一个很长的列表而言,最常使用的是其两端的数据,中间数据被访问的概率较低。因此,quicklist 允许将中间的节点使用 LZF 算法进行压缩以节省内存。

list-compress-depth 表示quicklist两端不被压缩的节点个数:

  • 0: 表示都不压缩。这是Redis的默认值。
  • 1: 表示quicklist两端各有1个节点不压缩,中间的节点压缩。
  • 2: 表示quicklist两端各有2个节点不压缩,中间的节点压缩。
  • 以此类推…

当集合中的元素均为整数且元素数少于 set-max-intset-entries 时,redis 采用 inset 编码存储集合。当插入非整数元素或元素数超过阈值后,intset 会升级为 hashtable 编码进行存储。

intset 是整数元素组成的有序数组, 可以支持 O(logn) 级别的查询。

intset 的内存结构与 ziplist 类似是一段的内存。它由三个部分组成:

  • encoding: 表示intset中的每个数据元素用几个字节来存储。它有三种可能的取值:
  • INTSET_ENC_INT16表示每个元素用2个字节存储
  • INTSET_ENC_INT32表示每个元素用4个字节存储
  • INTSET_ENC_INT64表示每个元素用8个字节存储。
  • length: 表示intset中的元素个数。encoding和length两个字段构成了intset的头部(header)。
  • contents: 表示实际存储的内容。它是一个C语言的柔性数组(flexible array member)

需要注意的是,每次添加元素 intset 都会检查是否需要将 INTSET_ENCODING 升级为更长的整数。与每个 entry 拥有独立 encoding 的 ziplist 不同,inset 中所有成员使用统一的 encoding。

Original: https://www.cnblogs.com/Finley/p/13423846.html
Author: -Finley-
Title: Redis 内存压缩原理

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/529392/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • netstat 命令查看端口状态详解

    转载请注明出处: netstat 可以查看服务器当前端口列表及指定端口的连接状态等; -t : 指明显示TCP端口,t是TCP的首字母。 -u : 指明显示UDP端口,u是UDP的…

    Linux 2023年6月14日
    074
  • Linux 安装Apache2和PHP

    首先先更新系统 sudo apt update; sudo apt upgrade 然后下载Apache2和PHP主要程序以及它的插件 sudo apt install apach…

    Linux 2023年6月7日
    083
  • 位图

    例题1 给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在 这40亿个数中。 首先肯定不能用传统的int数据存储 因为内存不够 40亿的整数大概为1…

    Linux 2023年6月13日
    078
  • redis详解(三)– 面试题(转载)

    使用redis有哪些好处? (1) 速度快,因为数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1) (2) 支持丰富数据类型,支持st…

    Linux 2023年5月28日
    096
  • 散列数据结构以及在HashMap中的应用

    1. 为什么需要散列表? 对于线性表和链表而言,访问表中的元素,时间复杂度均为O(n)。即便是通过树结构存储数据,时间复杂度也为O(logn)。那么有没有一种方式可以将这个时间复杂…

    Linux 2023年6月7日
    0118
  • docker-compose安装,yml文件配置

    1、离线安装 https://github.com/docker/compose/releases 移动文件 mv docker-compose-linux-x86_64 /usr…

    Linux 2023年5月27日
    093
  • C语言怎么给函数添加形参的默认值

    如果不是机缘巧合,当年转到C++之后,恐怕很难再有机会还写C的代码。面向对象在现代coding中,就像圣经一样,在码农的口中自带光环,code起来左一个语法糖,右一个范式编程,各种…

    Linux 2023年6月6日
    096
  • 数据链路层 交换机的工作原理

    以太网 以太网是一种将几台电脑连接起来,能够进行通讯的技术,也就是组建所谓的”局域网”。所以以太网可以说是一种局域网技术但局域网技术并非只有以太网一种,还有…

    Linux 2023年6月6日
    0105
  • mongodb压力测试工具ycsb

    mongodb安装 这里以安装单机版为例,rpm包方式安装 启动 ​ systemctl start mongod YCSB压测工具安装 这里不采用网上大多说的maven方式源码安…

    Linux 2023年6月14日
    078
  • 做任何事(决策)之前都要先考虑成本,再考虑收益

    所谓成本,就是我们在做一件事情时所付出的代价。 这个代价,或者说这个成本,有多有少,有显性有隐性,有我们知道的成本,也有我们不知道的成本。一切都是有成本的。 成本都有什么呢?一件事…

    Linux 2023年6月14日
    082
  • 树莓派4B串口测试与开发

    树莓派4B的串口,由两个增加4个,一共6个! 情况一: 缺省镜像中的配置,测试发现只启用了2个:pi@raspi4b:~ $ ls -l /dev/serial*lrwxrwxrw…

    Linux 2023年6月7日
    069
  • DotNet发布程序到NuGet

    1、新建一个类库 2、选择项目属性,在包栏目下填写 3、选择项目,鼠标右键”打包” 主要注意的是生成配置需改为 Release 4、然后就可以在我们项目 b…

    Linux 2023年6月13日
    088
  • KMP分析证明

    引用后缀的目的: “ABBABA” 如果说ABA里面组成的AB是答案组成部分的开头,那么AB后面的字符一定是和模式串开头的第三个字符一样,如果不一样一定不是…

    Linux 2023年6月7日
    060
  • tcpip详解-读书笔记

    TCP/IP详解 卷一 第一版读书笔记 第一章: 应用层关心是应用程序的细节,而不是数据在网络中对的传输活动,下三层对应用程序一无所知,但他们要处理所有的通信细节。 七层代理可以根…

    Linux 2023年6月13日
    086
  • 2021年3月-第02阶段-前端基础-Flex 伸缩布局-移动WEB开发_flex布局

    移动web开发——flex布局 1.0 传统布局和flex布局对比 1.1 传统布局 兼容性好 布局繁琐 局限性,不能再移动端很好的布局 1.2 flex布局 操作方便,布局极其简…

    Linux 2023年6月8日
    088
  • [非原创]2048游戏自动化算法

    function AI(grid) { this.grid = grid; } // static evaluation function AI.prototype.eval = …

    Linux 2023年6月13日
    086
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球