牺牲速度来节省内存,Redis是觉得自己太快了吗
前言
正常情况下我们选择使用 Redis
就是为了提升查询速度,然而让人意外的是,Redis
当中却有一种比较有意思的数据结构,这种数据结构通过牺牲部分读写速度来达到节省内存的目的,这就是 ziplist
(压缩列表),Redis
为什么要这么做呢?难道真的是觉得自己的速度太快了,牺牲一点速度也不影响吗?
什么是压缩列表
ziplist
是为了节省内存而设计出来的一种数据结构。ziplist
是由一系列特殊编码组成的连续内存块的顺序型数据结构,一个 ziplist
可以包含任意多个 entry
,而每一个 entry
又可以保存一个字节数组或者一个整数值。
ziplist
作为一种列表,其和普通的双端列表,如 linkedlist
的最大区别就是 ziplist
并不存储前后节点的指针,而 linkedlist
一般每个节点都会维护一个指向前置节点和一个指向后置节点的指针。那么 ziplist
不维护前后节点的指针,它又是如何寻找前后节点的呢?
ziplist
虽然不维护前后节点的指针,但是它却维护了上一个节点的长度和当前节点的长度,然后每次通过长度来计算出前后节点的位置。既然涉及到了计算,那么相对于直接存储指针的方式肯定有性能上的损耗,这就是一种典型的用时间来换取空间的做法。因为每次读取前后节点都需要经过计算才能得到前后节点的位置,所以会消耗更多的时间,而在 Redis
中,一个指针是占了 8
个字节,但是大部分情况下,如果直接存储长度是达不到 8
个字节的,所以采用存储长度的设计方式在大部分场景下是可以节省内存空间的。
ziplist 的存储结构
ziplist
的组成结构为:
<zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend>
其中 zlbytes
,zltail
,zllen
为 ziplist
的 head
部分,entry
为 ziplist
的 entries
部分,每一个 entry
代表一个数据,最后 zlend
表示 ziplist
的 end
部分,如下图所示:
ziplist
中每个属性代表的含义如下表格所示:
属性 | 类型 | 长 度 | 说明 |
---|---|---|---|
zlbytes | uint32_t | 4字节 | 记录压缩列表占用内存字节数(包括本身所占用的 4 个字节)。 |
zltail | uint32_t | 4字节 | 记录压缩列表尾节点距离压缩列表的起始地址有多少个字节(通过这个值可以计算出尾节点的地址) |
zllen | uint16_t | 2字节 | 记录压缩列表中包含的节点数量,当列表值超过可以存储的最大值(65535 )时,此值固定存储 65535 (即 2 的 16 次方 减 1 ),因此此时需要遍历整个压缩列表才能计算出真实节点数。 |
entry | 节点 | - | 压缩列表中的各个节点,长度由存储的实际数据决定。 |
zlend | uint8_t | 1字节 | 特殊字符 0xFF (即十进制 255 ),用来标记压缩列表的末端(其他正常的节点没有被标记为 255 的,因为 255 用来标识末尾,后面可以看到,正常节点都是标记为 254 )。 |
entry 存储结构
ziplist
的 head
和 end
存的都是长度和标记,而 entry
存储的是具体元素,这又是经过特殊的设计的一种存储格式,每个 entry
都以包含两段信息的元数据作为前缀,每一个 entry
的组成结构为:
<prevlen> <encoding> <entry-data>
prevlen
prevlen
属性存储了前一个 entry
的长度,通过此属性能够从后到前遍历列表。 prevlen
属性的长度可能是 1
字节也可能是 5
字节:
- 当链表的前一个
entry
占用字节数小于254
,此时prevlen
只用1
个字节进行表示。
<prevlen from 0 to 253> <encoding> <entry>
- 当链表的前一个
entry
占用字节数大于等于254
,此时prevlen
用5
个字节来表示,其中第1
个字节的值固定是254
(相当于是一个标记,代表后面跟了一个更大的值),后面4
个字节才是真正存储前一个entry
的占用字节数。
0xFE <4 bytes unsigned little endian prevlen> <encoding> <entry>
注意:1
个字节完全你能存储 255
的大小,之所以只取到 254
是因为 zlend
就是固定的 255
,所以 255
这个数要用来判断是否是 ziplist
的结尾。
encoding
encoding
属性存储了当前 entry
所保存数据的类型以及长度。encoding
长度为 1
字节,2
字节或者 5
字节长。前面我们提到,每一个 entry
中可以保存字节数组和整数,而 encoding
属性的第 1
个字节就是用来确定当前 entry
存储的是整数还是字节数组。当存储整数时,第 1
个字节的前两位总是 11
,而存储字节数组时,则可能是 00
、01
和 10
三种中的一种。
- 当存储整数时,第
1
个字节的前2
位固定为11
,其他位则用来记录整数值的类型或者整数值(下表所示的编码中前两位均为11
):
编码 | 长度 | entry保存的数据 |
---|---|---|
11000000 | 1字节 | int16_t类型整数 |
11010000 | 1字节 | int32_t类型整数 |
11100000 | 1字节 | int64_t类型整数 |
11110000 | 1字节 | 24位有符号整数 |
11111110 | 1字节 | 8位有符号整数 |
1111xxxx | 1字节 | xxxx 代表区间 0001-1101 ,存储了一个介于 0-12 之间的整数,此时 entry-data 属性被省略 |
注意:xxxx
四位编码范围是 0000-1111
,但是 0000
,1111
和 1110
已经被表格中前面表示的数据类型占用了,所以实际上的范围是 0001-1101
,此时能保存数据 1-13
,再减去 1
之后范围就是 0-12
。至于为什么要减去 1
是从使用习惯来说 0
是一个非常常用的数据,所以才会选择统一减去 1
来存储一个 0-12
的区间而不是直接存储 1-13
的区间。
- 当存储字节数组时,第
1
个字节的前2
位为00
、01
或者10
,其他位则用来记录字节数组的长度:
编码 | 长度 | entry保存的数据 |
---|---|---|
00pppppp | 1字节 | 长度小于等于 63 字节(6 位)的字节数组 |
01pppppp qqqqqqqq | 2字节 | 长度小于等于 16383 字节(14 位)的字节数组 |
10000000 qqqqqqqq rrrrrrrr ssssssss tttttttt | 5字节 | 长度小于等于 2 的 32 次方减 1 (32 位)的字节数组,其中第 1 个字节的后 6 位设置为 0 ,暂时没有用到,后面的 32 位(4 个字节)存储了数据 |
entry-data
entry-data
存储的是具体数据。当存储小整数(0-12
)时,因为 encoding
就是数据本身,此时 entry-data
部分会被省略,省略了 entry-data
部分之后的 ziplist
中的 entry
结构如下:
<prevlen> <encoding>
压缩列表中 entry
的数据结构定义如下(源码 ziplist.c
文件内),当然实际存储并没有直接使用到这个结构定义,这个结构只是用来接收数据,所以大家了解一下就可以了:
typedef struct zlentry {
unsigned int prevrawlensize;//存储prevrawlen所占用的字节数
unsigned int prevrawlen;//存储上一个链表节点需要的字节数
unsigned int lensize;//存储len所占用的字节数
unsigned int len;//存储链表当前节点的字节数
unsigned int headersize;//当前链表节点的头部大小(prevrawlensize + lensize)即非数据域的大小
unsigned char encoding;//编码方式
unsigned char *p;//指向当前节点的起始位置(因为列表内的数据也是一个字符串对象)
} zlentry;
ziplist 数据示例
上面讲解了大半天,可能大家都觉得枯燥无味了,也可能会觉得云里雾里,这个没有关系,这些只要心里有个概念,用到的时候再查询对应资料就可以了,并不需要全部记住,接下来让我们一起通过两个例子来体会一下 ziplist
到底是如何来组织存储数据的。
下面就是一个压缩列表的存储示例,这个压缩列表里面存储了 2
个节点,节点中存储的是整数 2
和 5
:
[0f 00 00 00] [0c 00 00 00] [02 00] [00 f3] [02 f6] [ff]
| | | | | |
zlbytes zltail zllen "2" "5" end
- 第一组
4
个字节为zlbytes
部分,0f
转成二进制就是1111
也就是15
,代表整个ziplist
长度是15
个字节。 - 第二组
4
个字节zltail
部分,0c
转成二进制就是1100
也就是12
,这里记录的是压缩列表尾节点距离起始地址有多少个字节,也就是就是说[02 f6]
这个尾节点距离起始位置有12
个字节。 - 第三组
2
个字节就是记录了当前ziplist
中entry
的数量,02
转成二进制就是10
,也就是说当前ziplist
有2
个节点。 - 第四组
2
个字节[00 f3]
就是第一个entry
,00
表示0
,因为这是第1
个节点,所以前一个节点长度为0
,f3
转成二进制就是11110011
,刚好对应了表格中的编码1111xxxx
,所以后面四位就是存储了一个0-12
位的整数。0011
转成十进制就是3
,减去1
得到2
,所以第一个entry
存储的数据就是2
。 - 第五组
2
个字节[02 f6]
就是第二个entry
,02
即为2
,表示前一个节点的长度为2
,注意,因为这里算出来的结果是小于254
,所以就代表了这里只用到了1
个字节来存储上一个节点的长度(如果等于254
,这说明接下来4
个字节才存储的是长度),所以后面的f6
就是当前节点的数据,转换成二进制为11110110
,对应了表格中的编码1111xxxx
,同样的后四位0110
存储的是真实数据,计算之后得出是5。 - 最后一组1个字节[ff]转成二进制就是
11111111
,代表这是整个ziplist
的结尾。
假如这时候又添加了一个 Hello World
字符串到列表中,那么就会新增一个 entry
,如下所示:
[02] [0b] [48 65 6c 6c 6f 20 57 6f 72 6c 64]
- 第一组的
1
个字节02
转成十进制就是2
,表示前一个节点(即上面示例中的[02 f6]
)长度是2
。 - 第 二组的
2
个字节0b
转成二进制为00001011
,以00
开头,符合编码00pppppp
,而除掉最开始的两位00
,计算之后得到十进制11
,这就说明后面字节数组的长度是11
。 - 第三组刚好是
11
个字节,对应了上面的长度,所以这里就是真正存储了Hello World
的字节数组。
ziplist 连锁更新问题
上面提到 entry
中的 prevlen
属性可能是 1
个字节也可能是 5
个字节,那么我们来设想这么一种场景:假设一个 ziplist
中,连续多个 entry
的长度都是一个接近但是又不到 254
的值(介于 250~253
之间),那么这时候 ziplist
中每个节点都只用了 1
个字节来存储上一个节点的长度,假如这时候添加了一个新节点,如 entry1
,其长度大于 254
个字节,此时 entry1
的下一个节点 entry2
的 prelen
属性就必须要由 1
个字节变为 5
个字节,也就是需要执行空间重分配,而此时 entry2
因为增加了 4
个字节,导致长度又大于 254
个字节了,那么它的下一个节点 entry3
的 prelen
属性也会被改变为 5
个字节。依此类推,这种产生连续多次空间重分配的现象就称之为连锁更新。同样的,不仅仅是新增节点,执行删除节点操作同样可能会发生连锁更新现象。
虽然 ziplist
可能会出现这种连锁更新的场景,但是一般如果只是发生在少数几个节点之间,那么并不会严重影响性能,而且这种场景发生的概率也比较低,所以实际使用时不用过于担心。
总结
本文主要讲解了 Redis
当中的 ziplist
(压缩列表),一种用时间换取空间的数据结构,在介绍压缩列表存储结构的同时通过一个存储示例来分析了 ziplist
是如何存储数据的,最后介绍了 ziplist
中可能发生的连锁更新问题。
牺牲速度来节省内存,Redis是觉得自己太快了吗的更多相关文章
- [PHP]引用返回与节省内存
PHP中的引用是什么:1.在 PHP 中引用意味着用不同的名字访问同一个变量内容2.引用可以被看作是 Unix 文件系统中的硬链接. 3.使用unset的话,只是删除他这个名字自身对内容的引用,并没有 ...
- python类与对象-如何为创建大量实例节省内存
如何为创建大量实例节省内存 问题举例 在网络游戏中,定义玩家类Player(id, name, level...), 每个玩家在线将创建一个Player实例,当在线人数很多时,将产生大量实例, 如何降 ...
- java内存缓存,节省内存
缓存的对象 这个问题就是我们上面提到的极端情况,在Java中,会对-128到127的Integer对象进行缓存,当创建新的Integer对象时,如果符合这个这个范围,并且已有存在的相同值的对象,则返回 ...
- 这种实现方式比使用 += 要更节省内存和 CPU,尤其是要串联的字符串数目特别多的时候。
这种实现方式比使用 += 要更节省内存和 CPU,尤其是要串联的字符串数目特别多的时候. package main import ( "bytes" "fmt" ...
- JS高级---构造函数通过原型添加方法,原型的作用: 共享数据, 节省内存空间
JS高级---构造函数,通过原型添加方法,原型的作用: 共享数据, 节省内存空间 构造函数 //构造函数 function Person(sex, age) { this.sex = sex; thi ...
- CPU缓存是位于CPU与内存之间的临时数据交换器,它的容量比内存小的多但是交换速度却比内存要快得多。CPU缓存一般直接跟CPU芯片集成或位于主板总线互连的独立芯片上
一.什么是CPU缓存 1. CPU缓存的来历 众所周知,CPU是计算机的大脑,它负责执行程序的指令,而内存负责存数据, 包括程序自身的数据.在很多年前,CPU的频率与内存总线的频率在同一层面上.内存的 ...
- 节省内存的循环banner(一)
循环banner是指scrollview首尾相连,循环播放的效果,使用非常广泛.例如淘宝的广告栏等. 如果是简单的做法可以把所有要显示的图片全部放进一个数组里,创建相同个数的图片视图来显示图片.这样的 ...
- 【转】java节省内存的几条建议
下面是参考网络资源总结的一些在Java编程中尽可能要做到的一些地方. 1. 尽量在合适的场合使用单例 使用单例可以减轻加载的负担,缩短加载的时间,提高加载的效率,但并不是所有地方都适用于单例,简单 ...
- 大数据量场景下storm自定义分组与Hbase预分区完美结合大幅度节省内存空间
前言:在系统中向hbase中插入数据时,常常通过设置region的预分区来防止大数据量插入的热点问题,提高数据插入的效率,同时可以减少当数据猛增时由于Region split带来的资源消耗.大量的预分 ...
随机推荐
- 【题解】P6329 【模板】点分树 | 震波
题外话 (其实模板题没必要在这里水题解的--主要是想说这个qwq) 小常数的快乐.jpg 我也不知道我为啥常数特别小跑得飞快--不加快读就能在 luogu 的最优解上跑到 rank5 ( 说不定深夜提 ...
- python绘折线图
# -*- coding: utf-8 -*- import numpy as np import matplotlib.pyplot as plt #X轴,Y轴数据 y = [0.3,0.4,2,5 ...
- Java JVM——2.类加载器子系统
概述 类加载器子系统在Java JVM中的位置 类加载器子系统的具体实现 类加载器子系统的作用 ① 负责从文件系统或者网络中加载.class文件,Class 文件在文件开头有特定的文件标识. ② Cl ...
- .NET Core集成CorrelationId实现全链路日志输出
.NET Core集成CorrelationId实现全链路日志输出 一,链路追踪 随着微服务架构的流行,一次请求会涉及多个服务的调用,并且服务本身也可能会依赖其他服务,整个请求路径会构成一个调用链,当 ...
- html 07-HTML5举例:简单的视频播放器
07-HTML5举例:简单的视频播放器 我们采用 Bootstrap 网站的图标字体,作为播放器的按钮图标. index.html的代码如下: <!DOCTYPE html> <ht ...
- CDH6.3.0下Apache Atlas2.1.0安装与配置
CDH6.3.0下Apache Atlas2.1.0安装与配置 0. 说明 文中的${ATLAS_HOME}, ${HIVE_HOME} 环境变更需要根据实际环境进行替换. 1. 依赖 A. 软件依赖 ...
- 【mybatis-plus】CRUD必备良药,mybatis的好搭档
做开发,免不了对数据进行增删改查,那么mybatis-plus我觉得很适合我这个java新手,简单好用. 官网在这 一.什么是mybatis-plus MyBatis-Plus(简称 MP),是一个M ...
- html嵌入腾讯视频的方法
1.首先我们从腾讯视频网站上找到一个视频网页的连接,格式是这样的 https://v.qq.com/x/page/b0136et5ztz.html 上面我标红色的是视频的vid 2.我们把vid放到接 ...
- webform中配置服务器控件的样式
前台 Style <asp:Label ID="Label1" runat="server" Text="Label" Style=& ...
- (九)rmdir和rm -r删除目录命令
一.命令描述与格式 rmdir用于删除空目录 命令格式 :rmdir [选项] 目录名 选项: --ignore-fail-on-non-empty :忽略任何因目录仍有数据而造成的错误 ...