redis数据结构-压缩列表

一 引入

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

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

Q3  ziplist类似于数组,ziplist在内存中是连续存储的。但是不同于数组,为了节省内存 ziplist的每个元素所占的内存大小可以不同(数组中叫元素,ziplist叫节点entry)。

普通数组的遍历是根据数组里存储的数据类型 找到下一个元素的,例如int类型的数组访问下一个元素时每次只需要移动一个sizeof(int)就行(实际上开发者只需让指针p+1就行,在这里引入sizeof(int)只是为了说明区别)。
上文说了,ziplist的每个节点的长度是可以不一样的,而我们面对不同长度的节点又不可能直接sizeof(entry),那么它是怎么访问下一个节点呢?

 

二 什么是压缩列表

压缩列表(ziplist )是一种数据结构,是Redis为了节约内存而开发的。

ziplist 是由一系列特殊编码组成的连续内存块的顺序型数据结构。

每个节点可以用来存储一个整数或者一个字符串。

.ziplist将一系列数据与其编码信息存储在一块连续的内存区域,这块内存物理上是连续的,逻辑上被分为多个组成部分,目的是在一定可控的时间复杂度条件下尽可能的减少不必要的内存开销(以时间换空间),从而达到节省内存的效果。

 

二 数据结构

2.1 压缩列表构成

《Redis设计与实现》


2.2 压缩列表节点构成

 2.2.1 previous_entry_length(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 的结尾。

 

2.2.2 encoding

在Redis的压缩表中,设计精髓都在这里,为了节省压缩表占用的内存,Redis对Encoding进行了极致的设计。一般都是读取前8个字节,用来判断存储的数据是什么。

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

当存储整数时,第 1 个字节的前 2 位固定为 11,其他位则用来记录整数值的类型或者整数值(下表所示的编码中前两位均为 11):

 

编码长度entry保存的数据
110000001字节int16_t类型整数
110100001字节int32_t类型整数
111000001字节int64_t类型整数
111100001字节24位有符号整数
111111101字节8位有符号整数
1111xxxx1字节xxxx 代表区间 0001-1101,存储了一个介于 0-12 之间的整数,此时 entry-data 属性被省略

 

注意:xxxx 四位编码范围是 0000-1111,但是 00001111 和 1110 已经被表格中前面表示的数据类型占用了,所以实际上的范围是 0001-1101,此时能保存数据 1-13,再减去 1 之后范围就是 0-12。至于为什么要减去 1 是从使用习惯来说 0 是一个非常常用的数据,所以才会选择统一减去 1 来存储一个 0-12 的区间而不是直接存储 1-13 的区间。

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

编码长度entry保存的数据
00pppppp1字节长度小于等于 63 字节(6 位)的字节数组
01pppppp qqqqqqqq2字节长度小于等于 16383 字节(14 位)的字节数组
10000000 qqqqqqqq rrrrrrrr ssssssss tttttttt5字节长度小于等于 2 的 32 次方减 1 (32 位)的字节数组,其中第 1 个字节的后 6 位设置为 0,暂时没有用到,后面的 32 位(4 个字节)存储了数据

2.2.3 content

2.3 查找

回答Q2的问题:

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

2.4 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 个字节。依此类推,这种产生连续多次空间重分配的现象就称之为连锁更新。同样的,不仅仅是新增节点,执行删除节点操作同样可能会发生连锁更新现象。

2.5 性能

(1)在Redis中,数据结构压缩表是紧凑排列的,所以,我们每次查询都需要遍历整一个列表,才能查询到相关数据,因为ziplist存放的个数非常有限,所以性能的开销并不大。

(2)虽然 ziplist 可能会出现这种连锁更新的场景,但是一般如果只是发生在少数几个节点之间,那么并不会严重影响性能,而且这种场景发生的概率也比较低,所以实际使用时不用过于担心。

三 应用

压缩列表(ziplist)是列表键和哈希键的底层实现之一。

当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现。
当一个哈希键只包含少量键值对,并且每个键值对的键和值要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做哈希键的底层实现。

Redis一共支持五种数据结构类型,其中有三种数据结构在一定条件下会应用压缩列表。

preview

 

四 问答

Q: Redis为什么使用压缩列表?使用压缩列表的好处是什么?压缩列表的应用对与我们使用内存有没有什么启发?

A: (1)    节约内存: 

   任何策略都有其应用场景,不同场景应用不同策略。

   Redis是一款内存数据库软件,想办法尽可能减少内存的开销是Redis设计者一定要考虑的事情

  压缩列表在一定N内可以容忍的时间复杂度,利用巧妙的编码技术除了存储内容尽可能的减少不必要的内存开销,将数据存储于连续的内存区域

(2)  减少内存碎片

    将很多小的数据块存储在一个比较大的内存区域。

   如果要存储的数据都是很小的条目,为每一个数据条目都单独的申请内存,结果是这些条目将有可能分散在内存的每一个角落,最终导致碎片增加,这是一件令人头疼的事情。

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值