牺牲速度来节省内存,Redis是觉得自己太快了吗

  • encoding

  • entry-data

  • ziplist 数据示例

  • ziplist 连锁更新问题

  • 总结

前言

===============================================================

本文GitHub已收录:https://zhouwenxing.github.io/

正常情况下我们选择使用 Redis 就是为了提升查询速度,然而让人意外的是,Redis 当中却有一种比较有意思的数据结构,这种数据结构通过牺牲部分读写速度来达到节省内存的目的,这就是 ziplist(压缩列表),Redis 为什么要这么做呢?难道真的是觉得自己的速度太快了,牺牲一点速度也不影响吗?

什么是压缩列表

====================================================================

ziplist 是为了节省内存而设计出来的一种数据结构。ziplist 是由一系列特殊编码组成的连续内存块的顺序型数据结构,一个 ziplist 可以包含任意多个 entry,而每一个 entry 又可以保存一个字节数组或者一个整数值。

ziplist 作为一种列表,其和普通的双端列表,如 linkedlist 的最大区别就是 ziplist 并不存储前后节点的指针,而 linkedlist 一般每个节点都会维护一个指向前置节点和一个指向后置节点的指针。那么 ziplist 不维护前后节点的指针,它又是如何寻找前后节点的呢?

ziplist 虽然不维护前后节点的指针,但是它却维护了上一个节点的长度和当前节点的长度,然后每次通过长度来计算出前后节点的位置。既然涉及到了计算,那么相对于直接存储指针的方式肯定有性能上的损耗,这就是一种典型的用时间来换取空间的做法。因为每次读取前后节点都需要经过计算才能得到前后节点的位置,所以会消耗更多的时间,而在 Redis 中,一个指针是占了 8 个字节,但是大部分情况下,如果直接存储长度是达不到 8 个字节的,所以采用存储长度的设计方式在大部分场景下是可以节省内存空间的。

ziplist 的存储结构

==========================================================================

ziplist 的组成结构为:

其中 zlbyteszltailzllenziplisthead 部分,entryziplistentries 部分,每一个 entry 代表一个数据,最后 zlend 表示 ziplistend 部分,如下图所示:

在这里插入图片描述

ziplist 中每个属性代表的含义如下表格所示:

| 属性 | 类型 | 长 度 | 说明 |

| — | :-- | — | — |

| zlbytes | uint32_t | 4字节 | 记录压缩列表占用内存字节数(包括本身所占用的 4 个字节)。 |

| zltail | uint32_t | 4字节 | 记录压缩列表尾节点距离压缩列表的起始地址有多少个字节(通过这个值可以计算出尾节点的地址) |

| zllen | uint16_t | 2字节 | 记录压缩列表中包含的节点数量,当列表值超过可以存储的最大值(65535)时,此值固定存储 65535(即 216 次方 减 1),因此此时需要遍历整个压缩列表才能计算出真实节点数。 |

| entry | 节点 | - | 压缩列表中的各个节点,长度由存储的实际数据决定。 |

| zlend | uint8_t | 1字节 | 特殊字符 0xFF(即十进制 255),用来标记压缩列表的末端(其他正常的节点没有被标记为255的,因为255用来标识末尾,后面可以看到,正常节点都是标记为254)。 |

entry 存储结构


ziplistheadend 存的都是长度和标记,而 entry 存储的是具体元素,这又是经过特殊的设计的一种存储格式,每个 entry 都以包含两段信息的元数据作为前缀,每一个 entry 的组成结构为:

prevlen

prevlen 属性存储了前一个 entry 的长度,通过此属性能够从后到前遍历列表。 prevlen 属性的长度可能是 1 字节也可能是 5 字节:

  • 当链表的前一个 entry 占用字节数小于 254,此时 prevlen 只用 1 个字节进行表示。

<prevlen from 0 to 253>

  • 当链表的前一个 entry 占用字节数大于等于 254,此时 prevlen5 个字节来表示,其中第 1 个字节的值固定是 254(相当于是一个标记,代表后面跟了一个更大的值),后面 4 个字节才是真正存储前一个 entry 的占用字节数。

0xFE <4 bytes unsigned little endian prevlen>

注意:1 个字节完全你能存储 255 的大小,之所以只取到 254 是因为 zlend 就是固定的 255,所以 255 这个数要用来判断是否是 ziplist 的结尾

encoding

encoding 属性存储了当前 entry 所保存数据的类型以及长度。encoding 长度为 1 字节,2 字节或者 5 字节长。前面我们提到,每一个 entry 中可以保存字节数组和整数,而 encoding 属性的第 1 个字节就是用来确定当前 entry 存储的是整数还是字节数组。当存储整数时,第 1 个字节的前两位总是 11,而存储字节数组时,则可能是 000110 三种中的一种。

  • 当存储整数时,第 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,但是 000011111110 已经被表格中前面表示的数据类型占用了,所以实际上的范围是 0001-1101,此时能保存数据 1-13,再减去 1 之后范围就是 0-12。至于为什么要减去 1 是从使用习惯来说 0 是一个非常常用的数据,所以才会选择统一减去 1 来存储一个 0-12 的区间而不是直接存储 1-13 的区间。

  • 当存储字节数组时,第 1 个字节的前 2 位为 0001 或者 10,其他位则用来记录字节数组的长度:

| 编码 | 长度 | entry保存的数据 |

| — | — | — |

| 00pppppp | 1字节 | 长度小于等于 63 字节(6 位)的字节数组 |

| 01pppppp qqqqqqqq | 2字节 | 长度小于等于 16383 字节(14 位)的字节数组 |

| 10000000 qqqqqqqq rrrrrrrr ssssssss tttttttt | 5字节 | 长度小于等于 232 次方减 132 位)的字节数组,其中第 1 个字节的后 6 位设置为 0,暂时没有用到,后面的 32 位(4 个字节)存储了数据 |

entry-data

entry-data 存储的是具体数据。当存储小整数(0-12)时,因为 encoding 就是数据本身,此时 entry-data 部分会被省略,省略了 entry-data 部分之后的 ziplist 中的 entry 结构如下:

压缩列表中 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;//编码方式

最后

现在正是金三银四的春招高潮,前阵子小编一直在搭建自己的网站,并整理了全套的**【一线互联网大厂Java核心面试题库+解析】:包括Java基础、异常、集合、并发编程、JVM、Spring全家桶、MyBatis、Redis、数据库、中间件MQ、Dubbo、Linux、Tomcat、ZooKeeper、Netty等等**

image

unsigned char encoding;//编码方式

最后

现在正是金三银四的春招高潮,前阵子小编一直在搭建自己的网站,并整理了全套的**【一线互联网大厂Java核心面试题库+解析】:包括Java基础、异常、集合、并发编程、JVM、Spring全家桶、MyBatis、Redis、数据库、中间件MQ、Dubbo、Linux、Tomcat、ZooKeeper、Netty等等**

[外链图片转存中…(img-yksZUamq-1721194391804)]

  • 23
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值