你需要知道的那些 redis 数据结构(前篇)

戳蓝字“CSDN云计算”关注我们哦! 

640?wx_fmt=jpeg
作者 | 饿了么物流技术团队
来源 | CSDN 企业博客

redis 对于团队中的同学们来说是非常熟悉的存在了,我们常用它来做缓存、或是实现分布式锁等等。对于其 api 中提供的几种数据结构,大家也使用得得心应手。

api 中的数据结构有:string、list、hash、set、sorted set。

这些 api 提供的“数据结构”,在 redis 的官方文档中有详细的介绍。就不多做展开,本次重点在于讨论 redis 数据结构的内部更底层的实现。如:

sds、adlist(在 3.2 版本中被 quicklist 所代替)、dict、skiplist、intset、ziplist和object。

在学习了解 redis 几个底层数据结构的过程中,处处可以体会到作者在设计 redis 时对于性能与空间的思考。

一、sds 简单动态字符串

1、sds 结构

redis 没有直接使用 C 语言传统的字符串表示(以空字符结尾的字符数组,以下简称 C 字符串), 而是自己构建了一种名为简单动态字符串(simple dynamic string,sds)的抽象类型,并将 sds 用作 redis 的默认字符串表示。

根据传统,C 语言使用长度为 N+1 的字符数组来表示长度为 N 的字符串, 并且字符数组的最后一个元素总是空字符 '\0' 。如下图:

640?wx_fmt=png

因为 C 字符串并不记录自身的长度信息,所以为了获取一个 C 字符串的长度,程序必须遍历整个字符串, 对遇到的每个字符进行计数,直到遇到代表字符串结尾的空字符为止,这个操作的复杂度为 O(N) 。

和 C 字符串不同,因为 sds 在 len 属性中记录了 sds 本身的长度,所以获取一个 sds 长度的复杂度仅为 O(1) 。与此同时,它还通过 alloc 属性记录了自己的总分配空间。下图为 sds 的数据结构:

640?wx_fmt=png

区别于 C 字符串,sds 有自己独特的 header,而且多达 5 种,结构如下:

typedef char *sds;	/* Note: sdshdr5 is never used, we just access the flags byte directly.	* However is here to document the layout of type 5 SDS strings. */	
struct __attribute__ ((__packed__)) sdshdr5 {	unsigned char flags; /* 3 lsb of type, and 5 msb of string length */	char buf[];	
};	
struct __attribute__ ((__packed__)) sdshdr8 {	uint8_t len; /* used */	uint8_t alloc; /* excluding the header and null terminator */	unsigned char flags; /* 3 lsb of type, 5 unused bits */	char buf[];	
};	
struct __attribute__ ((__packed__)) sdshdr16 {	uint16_t len; /* used */	uint16_t alloc; /* excluding the header and null terminator */	unsigned char flags; /* 3 lsb of type, 5 unused bits */	char buf[];	
};	
struct __attribute__ ((__packed__)) sdshdr32 {	uint32_t len; /* used */	uint32_t alloc; /* excluding the header and null terminator */	unsigned char flags; /* 3 lsb of type, 5 unused bits */	char buf[];	
};	
struct __attribute__ ((__packed__)) sdshdr64 {	uint64_t len; /* used */	uint64_t alloc; /* excluding the header and null terminator */	unsigned char flags; /* 3 lsb of type, 5 unused bits */	char buf[];	
};

之所以有 5 种,是为了能让不同长度的字符串可以使用不同大小的 header。这样,短字符串就能使用较小的 header,从而节省内存。

通过使用 sds 而不是 C 字符串,redis 将获取字符串长度所需的复杂度从 O(N) 降低到了 O(1) ,这是一种以空间换时间的策略,确保了获取字符串长度的工作不会成为 redis 的性能瓶颈。

2、内存分配策略

再来看 sds 的定义,它是简单动态字符串。可动态扩展内存也是它的特性之一。sds 表示的字符串其内容可以修改,也可以追加。在很多语言中字符串会分为 mutable 和 immutable 两种,显然 sds 属于 mutable 类型的。当 sds API 需要对 sds 进行修改时, API 会先检查 sds 的空间是否满足修改所需的要求, 如果不满足的话,API 会自动将 sds 的空间扩展至足以执行修改所需的大小,然后才执行实际的修改操作,所以使用 sds 既不需要手动修改 sds 的空间大小, 也不会出现 C 语言中可能面临的缓冲区溢出问题。

提到字符串变化就不得不提到内存重分配这个问题,对于一个 C 字符串,每次发生变更,程序都总要对保存个 C 字符串的数组进行一次内存重分配操作:

  • 如果程序执行的是增长字符串的操作,比如拼接操作(append),那么在执行这个操作之前, 程序需要先通过内存重分配来扩展底层数组的空间大小 —— 如果忘了这一步就会产生缓冲区溢出。

  • 如果程序执行的是缩短字符串的操作,比如截断操作(trim),那么在执行这个操作之后, 程序需要通过内存重分配来释放字符串不再使用的那部分空间 —— 如果忘了这一步就会产生内存泄漏。


因为内存重分配涉及复杂的算法,并且可能需要执行系统调用,所以它通常是一个比较耗时的操作:

  • 在一般程序中, 如果修改字符串长度的情况不太常出现, 那么每次修改都执行一次内存重分配是可以接受的。

  • 但是 redis 作为一个内存数据库, 经常被用于速度要求严苛、数据被频繁修改的场合, 如果每次修改字符串的长度都需要执行一次内存重分配的话, 那么光是执行内存重分配的时间就会占去修改字符串所用时间的一大部分, 如果这种修改频繁地发生的话, 可能还会对性能造成影响。

为了避免 C 字符串的这种缺陷,sds 通过未使用空间解除了字符串长度和底层数组长度之间的关联:在 sds 中,buf 数组的长度不一定就是字符数量加一,数组里面可以包含未使用的字节,而这些未使用字节的数量可以由 sds 的 alloc 属性减去len属性得到。

通过未使用空间,sds 实现了空间预分配和惰性空间释放两种优化策略。

空间预分配

空间预分配用于优化 sds 的字符串增长操作:当 sds 的 API 对一个 sds 进行修改,并且需要对 sds 进行空间扩展的时候,程序不仅会为 sds 分配修改所必须要的空间,还会为 sds 分配额外的未使用空间,并根据新分配的空间重新定义 sds 的 header。此部分的代码逻辑如下:

    /* Return ASAP if there is enough space left. */	if (avail >= addlen) return s;	len = sdslen(s);	sh = (char*)s-sdsHdrSize(oldtype);	newlen = (len+addlen);	if (newlen < SDS_MAX_PREALLOC)	newlen *= 2;	else	newlen += SDS_MAX_PREALLOC;	type = sdsReqType(newlen);	

简单来说就是:

如果对 sds 进行修改之后,sds 的长度(也即是 len 属性的值)将小于 1 MB ,那么程序分配和 len 属性同样大小的未使用空间,这时 SDSsdsalloc 属性的值将正好为 len 属性的值的两倍。举个例子, 如果进行修改之后,sds 的 len 将变成 13 字节,那么程序也会分配 13 字节的未使用空间,alloc 属性将变成 13字节,sds 的 buf 数组的实际长度将变成 13 + 13 + 1 = 27 字节(额外的一字节用于保存空字符)。

如果对 sds 进行修改之后,sds 的长度将大于等于 1 MB ,那么程序会分配 1 MB 的未使用空间。举个例子, 如果进行修改之后,sds 的 len 将变成 30 MB,那么程序会分配 1 MB 的未使用空间,alloc 属性将变成 31 MB ,sds 的 buf 数组的实际长度将为 30 MB + 1 MB + 1 byte。

通过空间预分配策略,Redis 可以减少连续执行字符串增长操作所需的内存重分配次数。通过这种空间换时间的预分配策略,sds 将连续增长 N 次字符串所需的内存重分配次数从必定 N 次降低为最多 N 次。

内存预分配策略仅在 sds 扩展的时候才触发,而新创建的 sds 长度和 C 字符串一致,是长度 + 1byte。

惰性空间释放

惰性空间释放用于优化 sds 的字符串缩短操作:当 sds 的 API 需要缩短 sds 保存的字符串时, 程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用 free 属性将这些字节的数量记录起来, 并等待将来使用。

通过惰性空间释放策略,sds 避免了缩短字符串时所需的内存重分配操作, 并为将来可能有的增长操作提供了优化。与此同时,sds 也提供了相应的 API sdsfree,让我们可以在有需要时, 真正地释放 sds 里面的未使用空间,所以不用担心惰性空间释放策略会造成内存浪费。源码如下:

/* Free an sds string. No operation is performed if 's' is NULL. */	
void sdsfree(sds s) {	if (s == NULL) return;	s_free((char*)s-sdsHdrSize(s[-1]));	
}

细想一下,惰性空间释放策略也是空间换时间策略的实现之一,作者对于性能的追求是非常执着的。当然也不是说为了性能,就不在乎内存的使用了,且看下一部分。

二、ziplist压缩链表

1、ziplist介绍

The ziplist is a specially encoded dually linked list that is designed to be very memory efficient. It stores both strings and integer values,where integers are encoded as actual integers instead of a series ofcharacters. It allows push and pop operations on either side of the list in O(1) time. However, because every operation requires a reallocation of the memory used by the ziplist, the actual complexity is related to the amount of memory used by the ziplist.

这是位于 ziplist.c 头部的一段介绍。翻译过来就是:ziplist 是一个经过特殊编码的双向链表,它的设计目标就是为了提高存储效率。ziplist 可以用于存储字符串或整数,其中整数是按真正的二进制表示进行编码的,而不是编码成字符串序列。它能以 O(1) 的时间复杂度在表的两端提供 push 和 pop 操作。然而,由于 ziplist 的每次变更操作都需要一次内存重分配,ziplist 实际的复杂度和其实际使用的内存量有关。

ziplist 充分体现了 Redis 对于存储效率的追求。一个普通的双向链表,链表中每一项都占用独立的一块内存,各项之间用地址指针(或引用)连接起来。这种方式会带来大量的内存碎片,而且地址指针也会占用额外的内存。而 ziplist 却是将表中每一项存放在前后连续的地址空间内,一个 ziplist 整体占用一大块内存。它是一个表(list),但其实不是一个链表(linked list) – zhangtielei

2、ziplist 结构

640?wx_fmt=png

ziplist 中的每个节点都以包含两个部分的元数据为前缀信息。首先,有 prevlen 存储前一个节点的长度,这提供了能够从尾到头遍历列。其次,encoding 表示了节点类型,是整数或是字符串,在本例中字符串也表示字符串有效负载的长度。所以完整的条目存储如下:

<prevlen> <encoding> <entry-data>

有的时候 encoding 也会用于表示节点数据本身,比如较小的整数,在这种情况下 节点会被省去,此时只需如下结构即可表示一个节点,这也是为节省内存而设计:

<prevlen> <encoding>

上一个节点的长度 <prevlen> 是按以下方式编码的:如果上一节点长度小于 254 字节,则它将只使用一个字节,表示长度为一个未指定的 8 位整数。当长度大于或等于 254 时,将消耗 5 个字节。第一个字节设置为 254(0xFE),表示后面的值较大。剩下的 4 个字节将前一个条目的长度作为值。

节点的的 encoding 字段取决于节点的内容。当该节点是一个字符串时,首先是编码的前 2 位 byte 将保存用于存储字符串长度的编码类型,后跟字符串的实际长度。当条目为整数时前 2 位都设置为 1,后 2 位用于指定此节点将存储哪种整数。不同 encoding 类型和编码如下。

|00pppppp| - 占用空间 1 byte	
表示长度小于等于63字节的字符串(6 bits)。	
如:"pppppp" 表示无符号6bit的字符串长度。	|01pppppp|qqqqqqqq| - 占用空间  2 bytes	
表示长度小于等于16383字节的字符串(14 bits)。	|10000000|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt| - 占用空间  5 bytes	
表示长度大等于16384字节的字符串(14 bits)。	
只有后面的4个字节表示长度,最多32^2-1。不使用第一个字节的6个低位,并且全部设置为零。	|11000000| - 占用空间  3 bytes	
后面两个字节表示 int16_t 的无符号整数 (2 bytes)。	|11010000| - 占用空间  5 bytes	
后面四个字节表示 int32_t 的无符号整数 (4 bytes)。	|11100000| - 占用空间 9 bytes	
后面八个字节表示 int32_t 的无符号整数 (8 bytes).	|11110000| - 占用空间 4 bytes	
后面三个字节表示24bits的有符号整数 (3 bytes).	|11111110| - 2 bytes	
后面一个字节表示8bits的有符号整数 (1 byte).	|1111xxxx| - (xxxx 在 0000 到 1101 之间) 的4bits整数.	
但是它其实只用来表示0到12,因为0000、1111、1110都已经被别的encoding使用过了,	
所以这种情况下需要用这4bit所对应的值减去1来获取它真实表示的值。	|11111111| - 表示ziplist结尾的特殊节点。	

其后的 entry-data 就用于存储 encoding 中定义的数据了。

总结一下:

  • ziplist 体现了 Redis 对于存储效率的追求,它是一种为节约内存而开发的顺序型数据结构。

  • ziplist 被用作列表键和哈希键的底层实现之一。

  • ziplist 可以包含多个节点,每个节点可以保存一个字节数组或者整数值。

  • ziplist 的设计为将各个数据项挨在一起组成连续的内存空间,这种结构并不擅长做修改操作。一旦数据发生改动,就会引发内存重分配。

三、本期总结

redis 在设计中并不是一味得追求性能,存储效率也是它追求的一个目标,不止 sds 和 ziplist,其他的底层数据结构也是在追求时间复杂度和空间效率这一目标中的产物。通过解析 redis 的数据结构设计,能更好的帮助我们理解 redis 使用过程中的执行过程和原理。

640?wx_fmt=png

福利
扫描添加小编微信,备注“姓名+公司职位”,加入【云计算学习交流群】,和志同道合的朋友们共同打卡学习!


推荐阅读:
  • Serverless 的喧哗与骚动
  • 如何提升员工体验 助力企业业务增长?这个棘手的问题终于被解决了!
  • 接班马云的为何是张勇?
  • 免费开源!新学期必收藏的AI学习资源,从课件、工具到源码都齐了
  • 值得收藏!16段代码入门Python循环语句
  • 我在快手认识了 4 位工程师,看到了快速发展的公司和员工如何彼此成就!
  • 幼儿识字从比特币开始? 小哥出了本区块链幼教书, 画风真泥石流……

真香,朕在看了!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/521800.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

对数周期天线hfss建模_HFSS也有金手指,FADDM招式详解

FADDM(Finite Array Domain Decomposition Method)即有限大阵区域分解法是HFSS针对周期阵列天线的一种高效仿真方法,这种方法不仅能提升天线阵列建模和求解的效率&#xff0c;还能保证仿真结果的精准度。FADDM的优势同样的硬件可求解更大规模的阵列与在HFSS全模型求解具有同样精…

Tensorflow快餐教程(7) - 梯度下降

摘要&#xff1a; 梯度下降梯度下降学习完基础知识和矩阵运算之后&#xff0c;我们再回头看下第一节讲的线性回归的代码&#xff1a;import tensorflow as tf import numpy as nptrX np.linspace(-1, 1, 101) trY 2 * trX np.random.randn(*trX.shape) * 0.33 # 创建一些线性…

腾讯物联网操作系统正式开源,最小体积仅1.8 KB

9月18日&#xff0c;腾讯宣布将开源自主研发的轻量级物联网实时操作系统TencentOS tiny。相比市场上其它系统&#xff0c;腾讯TencentOS tiny在资源占用、设备成本、功耗管理以及安全稳定等层面极具竞争力。该系统的开源可大幅降低物联网应用开发成本&#xff0c;提升开发效率&…

云栖大讲堂Java基础入门(三)- 阿里巴巴Java开发手册介绍

摘要&#xff1a; 本文带大家简单理解阿里巴巴Java开发手册中的规约内容以及P3C项目&#xff0c;可以帮助开发者扫描出所有潜在的代码隐患。在中间也聊了一些对于不同语言设计的理解&#xff0c;如何去看待语言的设计&#xff0c;其实是我们去学习一个语言的核心。演讲嘉宾简介…

你的数据安全么?Hadoop再曝安全漏洞| 黑客利用Hadoop Yarn资源管理系统未授权访问漏洞进行攻击

摘要&#xff1a; 4月30日&#xff0c;阿里云发现&#xff0c;俄罗斯黑客利用Hadoop Yarn资源管理系统REST API未授权访问漏洞进行攻击。 Hadoop是一款由Apache基金会推出的分布式系统框架&#xff0c;它通过著名的 MapReduce 算法进行分布式处理&#xff0c;Yarn是Hadoop集群的…

博文强识|进阶企业大咖

出品 | CSDN云计算 每个周三周五&#xff0c;和小编共同分享优秀博文&#xff0c;一起遨游在知识的海洋。 你需要知道的那些 redis 数据结构&#xff08;前篇&#xff09; redis 对于团队中的同学们来说是非常熟悉的存在了&#xff0c;我们常用它来做缓存、或是实现分布式锁等…

php 类似微信下拉菜单,微信小程序模拟下拉菜单开发实例

本文主要和大家分享微信小程序模拟下拉菜单开发实例&#xff0c;希望能帮助到大家。一.知识点1.实现动态显示和隐藏某个控件列表1data:{open:false},showitem:function(){this.setData({open:!this.data.open})},.display_show{display: block;}.display_none{display: none;}2…

算法导论 pdf_学习数据结构和算法最好的书是什么?

-----------通知&#xff1a;如果本站对你学习算法有帮助&#xff0c;请收藏网址&#xff0c;并推荐给你的朋友。由于 labuladong 的算法套路太火&#xff0c;很多人直接拿我的 GitHub 文章去开付费专栏&#xff0c;价格还不便宜。我这免费写给你看&#xff0c;多宣传原创作者是…

Tensorflow快餐教程(8) - 深度学习简史

摘要&#xff1a; 深度学习简史深度学习简史从机器学习流派说起如果要给机器学习划分流派的话&#xff0c;初步划分可以分为『归纳学习』和『统计学习』两大类。所谓『归纳学习』&#xff0c;就跟我们平时学习所用的归纳法差不多&#xff0c;也叫『从样例中学习』。归纳学习又分…

usb设备驱动程序(一)

代码&#xff1a; #include <linux/atomic.h> #include <linux/kernel.h> #include <linux/list.h> #include <linux/module.h> #include <linux/slab.h> #include <linux/usb.h> #include <linux/videodev2.h> #include <linux…

Tensorflow快餐教程(9) - 卷积

摘要&#xff1a; 卷积的计算方法卷积卷积就是滑动中提取特征的过程在数学中&#xff0c;卷积convolution是一种函数的定义。它是通过两个函数f和g生成第三个函数的一种数学算子&#xff0c;表征函数f与g经过翻转和平移的重叠部分的面积。其定义为&#xff1a;h(x)f(x)∗g(x)∫…

商家笑了 设计师哭了,京东+英特尔的AI这招太绝

戳蓝字“CSDN云计算”关注我们哦&#xff01;作者 | 刘丹出品 | CSDN云计算&#xff08;ID&#xff1a;CSDNcloud&#xff09;在如今“无促销不电商”的大环境熏陶下&#xff0c;商家需要榨干了脑浆想尽各种奇招&#xff0c;玩法虽多&#xff0c;但万变不离其宗。面对上万、甚至…

明显调用的表达式前的括号必须具有指针函数类型_每天三分钟带你搞懂C++基础Day5 处理类型 typedef、auto、decltype...

类型别名(type alias)一个名字&#xff0c;是某种类型的同义词。使用类型名有很多好处&#xff0c;能让复杂的类型名字变得简单明了&#xff0c;易于理解和使用。有两种方法可用于定义类型别名。传统的方法是使用关键字typedef :typedef double wages; //wages是double的同义词…

基于OGG Datahub插件将Oracle数据同步上云

摘要&#xff1a;随着数据规模的不断扩大&#xff0c;传统的RDBMS难以满足OLAP的需求&#xff0c;本文将介绍如何将Oracle的数据实时同步到阿里云的大数据处理平台当中&#xff0c;并利用大数据工具对数据进行分析。一、背景介绍随着数据规模的不断扩大&#xff0c;传统的RDBMS…

那些阿里的年轻人

摘要&#xff1a; 今天是年轻人的节日 十九年前&#xff0c;杭州城西一间狭小简陋的民房里 有一群年轻人 他们衣着朴素、口袋里也没什么钱 但每个人的眼神是坚定的、热烈的 他们每天挂在嘴边的 是梦想要做一件改变世界的事儿 1999年&#xff0c;一群杭州的年轻人离开北京&#…

php的添加语句怎么写,php修改语句怎么写

php修改语句是“update student set 字段1新值1,…where id $id”,…;”&#xff0c;其中update语句就是用于修改数据库表中的数据。推荐&#xff1a;《PHP视频教程》PHP sql修改语句语法&#xff1a;$sql “update student set 字段1新值1,…where id $id”,…;注意&#xff…

招人!入职阿里仅1年,我和做AI的程序员薪资翻了2倍!

最近在知乎上&#xff0c;关于AI的这个话题又被顶起来&#xff0c;其中&#xff0c;这条回答让人印象深刻&#xff1a;在这短短的一条信息里&#xff0c;无疑显示出&#xff1a;AI行业缺人&#xff0c;高端岗位80万年薪恐怕也招不来&#xff01;小编上周在一个AI群里&#xff0…

使用Unoconv和LibreOffice进行格式转换实现在线预览 doc,doxc,xls,xlsx,ppt,pptx 文件

此项目根据企业真实需求制作而成&#xff0c;希望能帮助大家解决在线预览的问题&#xff01; 此项目已开源&#xff0c;欢迎大家来STAR 软件版本SpringBoot2.2.2.RELEASELibreOffice6.3.2unoconv0.6文章目录一、配置管理① pom② yml③ controller④ 文件格式转换工具类FileFor…

关于CNN图像分类的一份综合设计指南

摘要&#xff1a; 本文是一篇关于使用CNN完成图像分类的综合设计指南&#xff0c;涵盖了一些模型设计、模型优化以及数据处理经验&#xff0c;是一份适合图像分类方向研究者参考的综合设计指南。对于计算机视觉任务而言&#xff0c;图像分类是其中的主要任务之一&#xff0c;比…

从GitHub中整理出来的15个最受欢迎的Python开源框架,你喜欢哪个

摘要&#xff1a; 从GitHub中整理出的15个最受欢迎的Python开源框架。这些框架包括事件I/O&#xff0c;OLAP&#xff0c;Web开发&#xff0c;高性能网络通信&#xff0c;测试&#xff0c;爬虫等。 Django: Python Web应用开发框架 Django 应该是最出名的Python框架&#xff0c;…