Redis之压缩列表ziplist

 

 

Redis是基于内存的nosql,有些场景下为了节省内存redis会用“时间”换“空间”。
ziplist就是很典型的例子。

ziplist是list键、hash键以及zset键的底层实现之一(3.0之后list键已经不直接用ziplist和linkedlist作为底层实现了,取而代之的是quicklist
这些键的常规底层实现如下:

  • list键:双向链表
  • hash键:字典dict
  • zset键:跳跃表zskiplist

但是当list键里包含的元素较少、并且每个元素要么是小整数要么是长度较小的字符串时,redis将会用ziplist作为list键的底层实现。同理hash和zset在这种场景下也会使用ziplist。

既然已有底层结构可以实现list、hash、zset键,为什么还要用ziplist呢?
当然是为了节省内存空间
我们先来看看ziplist是如何压缩的

原理

整体布局

ziplist是由一系列特殊编码的连续内存块组成的顺序存储结构,类似于数组,ziplist在内存中是连续存储的,但是不同于数组,为了节省内存 ziplist的每个元素所占的内存大小可以不同(数组中叫元素,ziplist叫节点entry,下文都用“节点”),每个节点可以用来存储一个整数或者一个字符串。
下图是ziplist在内存中的布局

 

  • zlbytes: ziplist的长度(单位: 字节),是一个32位无符号整数
  • zltail: ziplist最后一个节点的偏移量,反向遍历ziplist或者pop尾部节点的时候有用。
  • zllen: ziplist的节点(entry)个数
  • entry: 节点
  • zlend: 值为0xFF,用于标记ziplist的结尾

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

节点的布局(entry)

每个节点由三部分组成:prevlength、encoding、data

  • prevlengh: 记录上一个节点的长度,为了方便反向遍历ziplist
  • encoding: 当前节点的编码规则,下文会详细说
  • data: 当前节点的值,可以是数字或字符串 

为了节省内存,根据上一个节点的长度prevlength 可以将ziplist节点分为两类:

 

  • entry的前8位小于254,则这8位就表示上一个节点的长度
  • entry的前8位等于254,则意味着上一个节点的长度无法用8位表示,后面32位才是真实的prevlength。用254 不用255(11111111)作为分界是因为255是zlend的值,它用于判断ziplist是否到达尾部。

根据当前节点存储的数据类型及长度,可以将ziplist节点分为9类
其中整数节点分为6类: 

 整数节点的encoding的长度为8位,其中高2位用来区分整数节点和字符串节点(高2位为11时是整数节点),低6位用来区分整数节点的类型,定义如下:

#define ZIP_INT_16B (0xc0 | 0<<4)//整数data,占16位(2字节)
#define ZIP_INT_32B (0xc0 | 1<<4)//整数data,占32位(4字节)
#define ZIP_INT_64B (0xc0 | 2<<4)//整数data,占64位(8字节)
#define ZIP_INT_24B (0xc0 | 3<<4)//整数data,占24位(3字节)
#define ZIP_INT_8B 0xfe //整数data,占8位(1字节)
/* 4 bit integer immediate encoding */
//整数值1~13的节点没有data,encoding的低四位用来表示data
#define ZIP_INT_IMM_MASK 0x0f
#define ZIP_INT_IMM_MIN 0xf1    /* 11110001 */
#define ZIP_INT_IMM_MAX 0xfd    /* 11111101 */

值得注意的是 最后一种encoding是存储整数0~12的节点的encoding,它没有额外的data部分,encoding的高4位表示这个类型,低4位就是它的data。这种类型的节点的encoding大小介于ZIP_INT_24B与ZIP_INT_8B之间(1~13),但是为了表示整数0,取出低四位xxxx之后会将其-1作为实际的data值(0~12)。在函数zipLoadInteger中,我们可以看到这种类型节点的取值方法:

...} else if (encoding >= ZIP_INT_IMM_MIN && encoding <= ZIP_INT_IMM_MAX) {ret = (encoding & ZIP_INT_IMM_MASK)-1;}
...

字符串节点分为3类:

  • 当data小于63字节时(2^6),节点存为上图的第一种类型,高2位为00,低6位表示data的长度。
  • 当data小于16383字节时(2^14),节点存为上图的第二种类型,高2位为01,后续14位表示data的长度。
  • 当data小于4294967296字节时(2^32),节点存为上图的第二种类型,高2位为10,下一字节起连续32位表示data的长度。

上图可以看出:
不同于整数节点encoding永远是8位,字符串节点的encoding可以有8位、16位、40位三种长度
相同encoding类型的整数节点 data长度是固定的,但是相同encoding类型的字符串节点,data长度取决于encoding后半部分的值。

#define ZIP_STR_06B (0 << 6)//字符串data,最多有2^6字节(encoding后半部分的length有6位,length决定data有多少字节)
#define ZIP_STR_14B (1 << 6)//字符串data,最多有2^14字节
#define ZIP_STR_32B (2 << 6)//字符串data,最多有2^32字节

 

上文介绍了ziplist节点(entry)的分类,知道了节点可以细分为9种类型,那么当遍历一个ziplist时,指针到达某个节点时 如何判断出节点的类型从而找到data呢?

已知节点的位置,求data的值

根据图2 entry布局 可以看出,若要算出data的偏移量,得先计算出prevlength所占内存大小(1字节和5字节):

//根据ptr指向的entry,返回这个entry的prevlensize
#define ZIP_DECODE_PREVLENSIZE(ptr, prevlensize) do {                          \if ((ptr)[0] < ZIP_BIGLEN) {                                               \(prevlensize) = 1;                                                     \} else {                                                                   \(prevlensize) = 5;                                                     \}                                                                          \
} while(0);

接着再用ZIP_DECODE_LENGTH(ptr + prevlensize, encoding, lensize, len)算出encoding所占的字节,返回给lensize;data所占的字节返回给len

//根据ptr指向的entry求出该entry的len(encoding里存的 data所占字节)和lensize(encoding所占的字节)
#define ZIP_DECODE_LENGTH(ptr, encoding, lensize, len) do {                    \ZIP_ENTRY_ENCODING((ptr), (encoding));                                     \if ((encoding) < ZIP_STR_MASK) {                                           \if ((encoding) == ZIP_STR_06B) {                                       \(lensize) = 1;                                                     \(len) = (ptr)[0] & 0x3f;                                           \} else if ((encoding) == ZIP_STR_14B) {                                \(lensize) = 2;                                                     \(len) = (((ptr)[0] & 0x3f) << 8) | (ptr)[1];                       \} else if (encoding == ZIP_STR_32B) {                                  \(lensize) = 5;                                                     \(len) = ((ptr)[1] << 24) |                                         \((ptr)[2] << 16) |                                         \((ptr)[3] <<  8) |                                         \((ptr)[4]);                                                \} else {                                                               \assert(NULL);                                                      \}                                                                      \} else {                                                                   \(lensize) = 1;                                                         \(len) = zipIntSize(encoding);                                          \}                                                                          \
} while(0);//将ptr的encoding解析成1个字节:00000000、01000000、10000000(字符串类型)和11??????(整数类型)
//如果是整数类型,encoding直接照抄ptr的;如果是字符串类型,encoding被截断成一个字节并清零后6位
#define ZIP_ENTRY_ENCODING(ptr, encoding) do {  \(encoding) = (ptr[0]); \if ((encoding) < ZIP_STR_MASK) (encoding) &= ZIP_STR_MASK; \
} while(0)//根据encoding返回数据(整数)所占字节数
unsigned int zipIntSize(unsigned char encoding) {switch(encoding) {case ZIP_INT_8B:  return 1;case ZIP_INT_16B: return 2;case ZIP_INT_24B: return 3;case ZIP_INT_32B: return 4;case ZIP_INT_64B: return 8;default: return 0; /* 4 bit immediate */}assert(NULL);return 0;
}

完成以上步骤之后,即可算出data的位置:ptr+prevlensize+lensize,以及data的长度len

ziplist接口

上文已经阐述了ziplist的底层内存布局,接下来看看一些基本的增删改查操作在ziplist中是如何执行的。

ziplistNew 创建一个ziplist O(1)

/* Create a new empty ziplist. */
unsigned char *ziplistNew(void) {unsigned int bytes = ZIPLIST_HEADER_SIZE+1;//<zlbytes>4字节<zltail>4字节<zllen>2字节<zlend>1字节,没有entry节点unsigned char *zl = zmalloc(bytes);ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);//<zlbytes>赋值ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);//<zltail>ZIPLIST_LENGTH(zl) = 0;//<zllen>zl[bytes-1] = ZIP_END;//<zlend>return zl;
}
#define ZIPLIST_HEADER_SIZE     (sizeof(uint32_t)*2+sizeof(uint16_t))//空ziplist除了<zlend>的大小
#define ZIPLIST_BYTES(zl)       (*((uint32_t*)(zl)))//<zlbyte>的指针的值,可读可写
#define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t*)((zl)+sizeof(uint32_t))))//<zltail>的指针的值
#define ZIPLIST_HEADER_SIZE     (sizeof(uint32_t)*2+sizeof(uint16_t))//空ziplist除了<zlend>的大小
#define ZIPLIST_LENGTH(zl)      (*((uint16_t*)((zl)+sizeof(uint32_t)*2)))//<zllen>的指针的值

参照着图1理解会直观些,分配了一块内存并初始化<zlbytes><zltail><zllen><zlend>,没有entry。

ziplistFind 从ziplist里找出一个entry O(n)

//返回p节点之后data与vstr(长度是vlen)相等的节点,只找p节点之后每隔skip的节点
//时间复杂度 O(n)
unsigned char *ziplistFind(unsigned char *p, unsigned char *vstr, unsigned int vlen, unsigned int skip) {int skipcnt = 0;unsigned char vencoding = 0;long long vll = 0;while (p[0] != ZIP_END) {unsigned int prevlensize, encoding, lensize, len;unsigned char *q;ZIP_DECODE_PREVLENSIZE(p, prevlensize);ZIP_DECODE_LENGTH(p + prevlensize, encoding, lensize, len);q = p + prevlensize + lensize;//当前节点的dataif (skipcnt == 0) {/* Compare current entry with specified entry */if (ZIP_IS_STR(encoding)) {//判断当前节点是不是字符串节点if (len == vlen && memcmp(q, vstr, vlen) == 0) {return p;}} else {/* Find out if the searched field can be encoded. Note that* we do it only the first time, once done vencoding is set* to non-zero and vll is set to the integer value. */if (vencoding == 0) {//这个代码块只会执行一次,计算vstr的整数表示if (!zipTryEncoding(vstr, vlen, &vll, &vencoding)) {//将参数给的节点vstr当做整数节点转换;将data值返回给vll,节点编码返回给vencoding//进入这个代码块说明将vstr转换成整数失败,vencoding不变,下次判断当前节点是整数节点之后可以跳过这个节点/* If the entry can't be encoded we set it to* UCHAR_MAX so that we don't retry again the next* time. */vencoding = UCHAR_MAX;//当前节点是整数节点,但是vstr是字符串节点,跳过不用比较了}/* Must be non-zero by now */assert(vencoding);}/* Compare current entry with specified entry, do it only* if vencoding != UCHAR_MAX because if there is no encoding* possible for the field it can't be a valid integer. */if (vencoding != UCHAR_MAX) {long long ll = zipLoadInteger(q, encoding);//算出当前节点的dataif (ll == vll) {return p;}}}/* Reset skip count */skipcnt = skip;} else {/* Skip entry */skipcnt--;}/* Move to next entry */p = q + len;}return NULL;
}//尝试将entry地址的内容转换成整数,并根据这个整数算出一个合适的encoding返回给encoding参数。
//若无法转换成整数,则encoding不变,返回0,等到下次调用zipEncodeLength时再计算一个该字符串的encoding
int zipTryEncoding(unsigned char *entry, unsigned int entrylen, long long *v, unsigned char *encoding) {long long value;if (entrylen >= 32 || entrylen == 0) return 0;if (string2ll((char*)entry,entrylen,&value)) {/* Great, the string can be encoded. Check what's the smallest* of our encoding types that can hold this value. */if (value >= 0 && value <= 12) {*encoding = ZIP_INT_IMM_MIN+value;} else if (value >= INT8_MIN && value <= INT8_MAX) {*encoding = ZIP_INT_8B;} else if (value >= INT16_MIN && value <= INT16_MAX) {*encoding = ZIP_INT_16B;} else if (value >= INT24_MIN && value <= INT24_MAX) {*encoding = ZIP_INT_24B;} else if (value >= INT32_MIN && value <= INT32_MAX) {*encoding = ZIP_INT_32B;} else {*encoding = ZIP_INT_64B;}*v = value;return 1;}return 0;
}/* Read integer encoded as 'encoding' from 'p' */
int64_t zipLoadInteger(unsigned char *p, unsigned char encoding) {int16_t i16;int32_t i32;int64_t i64, ret = 0;if (encoding == ZIP_INT_8B) {ret = ((int8_t*)p)[0];} else if (encoding == ZIP_INT_16B) {memcpy(&i16,p,sizeof(i16));memrev16ifbe(&i16);ret = i16;} else if (encoding == ZIP_INT_32B) {memcpy(&i32,p,sizeof(i32));memrev32ifbe(&i32);ret = i32;} else if (encoding == ZIP_INT_24B) {i32 = 0;memcpy(((uint8_t*)&i32)+1,p,sizeof(i32)-sizeof(uint8_t));memrev32ifbe(&i32);ret = i32>>8;} else if (encoding == ZIP_INT_64B) {memcpy(&i64,p,sizeof(i64));memrev64ifbe(&i64);ret = i64;} else if (encoding >= ZIP_INT_IMM_MIN && encoding <= ZIP_INT_IMM_MAX) {ret = (encoding & ZIP_INT_IMM_MASK)-1;} else {assert(NULL);}return ret;
}
其他接口
  • ziplistInsert 往ziplist里插入一个entry 时间复杂度 平均:O(n), 最坏:O(n²)
  • ziplistDelete 从siplist里删除一个entry 时间复杂度 平均:O(n), 最坏:O(n²)

为什么插入节点和删除节点两个接口的最坏时间复杂度会是O(n²)呢?这是由于ziplist的“连锁更新”导致的,连锁更新在最坏情况下需要对ziplist执行n次空间重分配操作,而且每次空间重分配的最坏时间复杂度为O(n) ----《Redis设计与实现》
但是出现“连锁更新”的情况并不多见,所以这里基本不会造成性能问题。
篇幅有限这里不能细说连锁更新,感兴趣可以阅读《Redis设计与实现》的相关章节以及ziplist.c里的__ziplistCascadeUpdate()函数。

总结

  • ziplist是为节省内存空间而生的。
  • ziplist是一个为Redis专门提供的底层数据结构之一,本身可以有序也可以无序。当作为listhash的底层实现时,节点之间没有顺序;当作为zset的底层实现时,节点之间会按照大小顺序排列。

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

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

相关文章

GLSL学习笔记

GLSL语言内置的变量&#xff0c;包括内置的顶点属性&#xff08;attribute&#xff09;、一致变量&#xff08;uniform&#xff09;、易变变量&#xff08;varying&#xff09;以及常量&#xff08;const&#xff09;&#xff0c;一方面加深印象&#xff0c;另一方面今天的文章…

SQL Server 文件规划 -位置规划

数据库设计中&#xff0c;文件规划是相当重要的一个环节。 这部分内容包括文件数目的规划&#xff0c;大小的规划&#xff0c;位置的规划。 本篇介绍的是文件位置的规划&#xff0c;如下图所示 1. 数据文件 有可能的情况下&#xff0c;应该区分经常更新的表与不经常更新的表。分…

使用AJAX Toolkit创建新闻列表

我们很多站点上面都需要显示新闻列表&#xff0c;由标题和正文组成的。一般客户都希望实现这样的效果&#xff1a; 开始的时候只是显示标题&#xff0c;当点击标题的时候&#xff0c;再展开正文。再点击&#xff0c;又可缩回去。 这是典型的AJAX效果&#xff0c;或者说以前你也…

生活规则

1.朋友请你吃饭&#xff0c;不要觉得理所当然&#xff0c;请礼尚往来&#xff0c;否则你的名声会越来越臭。 2.给自己定目标&#xff0c;一年&#xff0c;两年&#xff0c;五年&#xff0c;也许你出生不如别人好&#xff0c;通过努力&#xff0c;往往可以改变70%的命运。破罐子…

[AX]AX2012 SSRS报表使用Report Data Method

在AX2012的SSRS报表中可以使用c#或者Visual basic .net编写report data method来获取和操作数据&#xff0c;由report data method返回的数据可以用在报表的表达式中&#xff0c;也可以用作dataset的数据源。 使用Report data method首先需要创建AX model工程&#xff0c;在工程…

为什么分布式一定要有redis,redis的一些优缺点

1、为什么使用redis 分析:博主觉得在项目中使用redis&#xff0c;主要是从两个角度去考虑:性能和并发。当然&#xff0c;redis还具备可以做分布式锁等其他功能&#xff0c;但是如果只是为了分布式锁这些其他功能&#xff0c;完全还有其他中间件(如zookpeer等)代替&#xff0c;…

Google protobuf使用技巧和经验

Google protobuf是非常出色的开源工具&#xff0c;在项目中可以用它来作为服务间数据交互的接口&#xff0c;例如rpc服务、数据文件传输等。protobuf为proto文件中定义的对象提供了标准的序列化和反序列化方法&#xff0c;可以很方便的对pb对象进行各种解析和转换。以下是我总结…

show部分书...

继续购入中 转载于:https://www.cnblogs.com/Clingingboy/archive/2009/06/09/1499816.html

HTTP_POST———使用mysql_udf与curl库完成http_post通信模块(mysql_udf,multi_curl,http,post)...

HTTP_POST———使用mysql_udf与curl库完成http_post通信模块&#xff08;mysql_udf,multi_curl,http,post&#xff09; 这个模块其目前主要用于xoyo江湖的sns与kingsoft_xoyo自主研发的TCSQL数据库做数据同步&#xff0c;当有feed插入sns数据库&#xff0c;使用触 发器调用该模…

LevelDb实现原理

原文地址&#xff1a;http://www.samecity.com/blog/Index.asp?SortID12&#xff0c; 最近由于工作上的需求&#xff0c;需要用到leveldb&#xff0c;因此转载此文章用于以后的查询使用。 LevelDb日知录之一&#xff1a;LevelDb 101 说起LevelDb也许您不清楚&#xff0c;但是…

排序 八种经典排序算法

排序(Sorting) 是计算机程序设计中的一种重要操作&#xff0c;它的功能是将一个数据元素(或记录)的任意序列&#xff0c;重新排列成一个关键字有序的序列。 我整理了以前自己所写的一些排序算法结合网上的一些资料&#xff0c;共介绍8种常用的排序算法&#xff0c;希望对大家能…

Redis使用过程出现类型转换异常问题- 20190220

问题描述&#xff1a; 使用redis过程中&#xff0c;出现类型转换异常问题&#xff0c;出现在存数据和取数据时。而且相同代码在本地测试无异常&#xff0c;而提交到测试环境&#xff0c;则会出现问题。 问题原因&#xff1a; 最后定位到&#xff0c;原因在使用redis存取数据时&…

表达式求值Spring.Expressions

简介Spring.Expressions命名空间可以用一种强大的表达式语言在运行时操作对象。这种语言可以读写属性值、调用方法、访问数组/集合/索引器的元素、进行算术和逻辑运算&#xff0c;同时支持命名变量&#xff0c;并且能够通过名称从IoC容器获取对象。 在Spring.NET中&#xff0c…

MarshalByRefObject 的DOME代码

今天研究了下MarshalByRefObject跨程序通讯&#xff0c;由于今天很晚了&#xff0c;先贴出DOME代码。 分别建立2个winform程序&#xff0c;WinClient和WinServer&#xff0c;2个项目中都有CommunicationInfo类&#xff08;你也可以将CommunicationInfo做成一个类库供2个winform…

坐地铁的好心MM们小心啊,周末刚经历了一个地铁新骗术

先废话少说&#xff0c;直奔主题。这个周日&#xff0c;也就是昨天&#xff0c;很开心的和BF坐5号线去东单看电影&#xff0c;在惠新西街南口那站上来一个小姑娘&#xff0c;也就11&#xff0c;12岁那样&#xff0c;穿着小背心、短裤&#xff0c;脖子上还挂着类似学生证一类的牌…

XNA中的Render State管理

XNA中的Render State管理 仅供个人学习使用&#xff0c;请勿转载&#xff0c;勿用于任何商业用途。 The Problem&#xff1a; XNA中一个设计的非常不好的地方&#xff0c;就是把各种render state定义为RenderState类的成员&#xff0c;而不是枚举。在DX/MDX中&#xff0c…

统一项目管理平台(UMPlatForm.NET)-4.7 组织机构管理模块

统一项目管理平台&#xff08;UMPlatForm.NET&#xff09; 4.7 组织机构管理模块 统一项目管理平台&#xff08;UMPlatForm.NET&#xff09;,基于.NET的快速开发、整合框架。 4.7 组织机构管理模块 组织机构管理模块提供直观方便的组织机构管理&#xff0c;以树型结构显示单位和…

开源GIS系统关系图

转载于:https://www.cnblogs.com/penglink/archive/2009/06/26/1511373.html

c++常用知识点,易错点,面试常问点

1.谈谈你对C内存分配的理解1.1 还是的先看看C对内存分为哪几个区&#xff1f;1、栈区&#xff08;stack&#xff09;— 由编译器自动分配释放 &#xff0c;存放函数的参数值&#xff0c;局部变量的值等。其操作方式类似于数据结构中的栈。想知道为什么效率高吗&#xff1f;因为…

利用ACS来实现AAA服务

ACS简介思科安全访问控制服务器&#xff08;Cisco Secure Access Control Sever&#xff09;是一个高度可扩展、高性能的访问控制服务器&#xff0c;提供了全面的身份识别网络解决方案&#xff0c;是思科基于身份的网络服务(IBNS)架构的重要组件。Cisco Secure ACS通过在一个集…