Redis系列(三)底层数据结构之压缩列表

前言

Redis 已是你们耳熟能详的东西了,平常工做也都在使用,面试中也是高频的会涉及到,那么咱们对它究竟了解有多深入呢?前端

我读了几本 Redis 相关的书籍,尝试去了解它的具体实现,将一些底层的数据结构及实现原理记录下来。面试

本文将介绍 Redis 中底层的 ziplist(压缩列表) 的实现方法。 它是 Redis 中列表键和哈希键的底层实现之一。当符合某些状况(后续文章讲)时,列表键和哈希键会使用它。后端

2020-01-05-14-57-46

能够看到图中,这个键值为zsetkey的 zset 内部使用的编码方法就是 ziplist.数组

定义

列表数据结构,咱们已经有了链表,为何还须要从新搞一个压缩列表呢?为了节省内存微信

链表的先后指针是一个很是耗费内存的结构,所以在数据量小的时候,这一部分的空间尤为显得浪费。数据结构

压缩列表是一系列特殊编码的连续内存块组成的顺序性数据结构。性能

这句话有点绕口,其实核心思想就是,在一块连续的内存中,模拟出一个列表的结构。学习

压缩列表的定义

压缩列表的定义为:编码

struct ziplist<T>{
    // 整个压缩列表占用字节数
    int32 zlbytes;
    // 最后一个节点到压缩列表起始位置的偏移量,能够用来快速的定位到压缩列表中的最后一个元素
    int32 zltail_offset;
    // 压缩列表包含的元素个数
    int16 zllength;
    // 元素内容列表,用数组存储,内存上紧挨着
    T[] entries;
    // 压缩列表的结束标志位,值永远为 0xFF.
    int8 zlend;
}
复制代码

2020-01-05-15-12-25

每一个字段的含义已经注释在代码中了。这里额外解释一下为何须要 zltail_offset这个属性,由于压缩列表只能顺序遍历,因此为了提高效率,咱们须要能够从首尾双端来遍历,用这个属性能够很快的找到压缩列表的尾部。至于如何反向遍历,请继续向下看。url

压缩列表节点的定义

压缩列表的每个节点的定义为:

struct entry{
    // 前一个 entry 的长度
    int<var> prevlous_entry_length;
    // 编码方式
    int<vat> encoding;
    // 内容
    optional bute[] content;
}
复制代码
  • prevlous_entry_length

定义里,prevlous_entry_length 属性,就是为了反向遍历而记录的。想一下,首先拿到尾部节点的偏移量,找到最尾部的节点,而后调用prevlous_entry_length属性,就能够拿到前一个节点,而后不断向前遍历了。

这里须要注意的是:这个字段的长度并非必定的,它能够是 1 个字节,也能够是 5 个字节。

当前一个 entry 的长度在 254 字节之内的时候,这个属性用一个字节来记录。 不然就会用 5 个字节来记录。

这回致使一个问题,见后文。

  • encoding

这个属性记录了节点的 content 属性所保存的数据的类型以及长度。

  • content

这个属性用来真正的保存节点的值,能够是一个字节数组或者整数。它的类型和长度由 encoding 来决定。

新增节点

在前言里提到了,在某些状况下列表键会使用压缩列表,就是在列表键的内容比较少时,那么压缩列表为何不能用于大的列表键呢?

ziplist 是连续存储的数据结构,内存是没有冗余的(前面的文章讲过的 SDS 中就有冗余空间), 也就是说,每一次新增节点,都须要进行内存申请,而后将若是当前内存连续块够用,那么将新节点添加,若是申请到的是另一块连续内存空间,那么须要将全部的内容拷贝到新的地址。

也就是说,每一次新增节点,都须要内存分配,可能还须要进行内存拷贝。当 ziplist 中存储的值太多,内存拷贝将是一个很大的消耗。

也是所以,Redis 只在一些数据量小的场景下使用 ziplist.

问题:级联更新

在讲prevlous_entry_length的时候,咱们提到它的长度变化会致使一个问题,那就是级联更新。

当前一个 entry 的长度在 254 字节之内的时候,这个属性用一个字节来记录。不然就会用 5 个字节来记录。

那么咱们设想一个极端的场景,在这个 ziplist 内部,全部的节点的长度都是 253 字节,也就意味着全部节点的prevlous_entry_length属性都是一个字节。

此时,咱们给压缩列表最前端插入一个大于 254 字节的节点,那么此时原来的第一个节点的prevlous_entry_length属性会从 1 个字节变成 5 个字节,这个节点的总长度也就来到了 257 字节,大于 254 字节,那么下一个节点(原来的第二个节点)的prevlous_entry_length属性也会变成 5 个字节,这又会致使下一个节点的变化。... 引发连锁变化,全部节点的prevlous_entry_length值都须要更新一遍。

级联更新的时间复杂度不好,最多须要进行 N 次空间的重分配,每次空间的重分配最差须要 O(N), 因此级联更新的时间复杂度最差是 O(N2).

与新增节点类似,删除节点也有可能会形成级联更新的状况。

可是其实不用怕,由于级联更新形成 Redis 性能压力的几率极其低。

首先,级联更新须要连续的节点大小为250-253之间,这本就少见,而大范围的连续就更加少见了。若是运气很差出现了三五个的级联更新,也毫不会对 Redis 的性能有压力。

总结

ziplist 是 Redis 单独开发,用连续的内存空间来存储 list 的一个数据结构。它的优点是没有链表的先后指针的内存占用,可是在数据量大的时候,性能有压力。所以只用于数据量小的场景。

ziplist 是 list 键和 hash 键的底层实现数据结构之一。

ziplist 有一个问题,就是添加节点或者删除节点,有极小的几率会触发级联更新,引发性能差别。可是这个事真的极小几率,不用担忧。

参考文章

《Redis 的设计与实现(第二版)》

《Redis 深度历险:核心原理和应用实践》

完。

联系我

最后,欢迎关注个人我的公众号【 呼延十 】,会不按期更新不少后端工程师的学习笔记。 也欢迎直接公众号私信或者邮箱联系我,必定知无不言,言无不尽。


以上皆为我的所思所得,若有错误欢迎评论区指正。

欢迎转载,烦请署名并保留原文连接。

联系邮箱:huyanshi2580@gmail.com

更多学习笔记见我的博客或关注微信公众号 < 呼延十 >------>呼延十

相关文章
相关标签/搜索