Redis基本数据结构ZipList
为什么要有ziplist
有两点原因:
- 普通的双向链表,会有两个指针,在存储数据很小的情况下,我们存储的实际数据的大小可能还没有指针占用的内存大,是不是有点得不偿失?而且Redis是基于内存的,而且是常驻内存的,为了节省内存,又能达到链表的功能,ziplist出现了。
- 链表在内存中,一般是不连续的,遍历相对比较慢,而ziplist可以很好的解决这个问题。
ZipList
不用指针实现双端链表,具备双端链表的特性,可在任意一端进行压入,弹出操作
#define ZIP_END 255
#define ZIPLIST_BYTES(zl) (*((uint32_t*)(zl))) // 获取ziplist的bytes指针
#define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t*)((zl)+sizeof(uint32_t)))) // 获取ziplist的tail指针
#define ZIPLIST_LENGTH(zl) (*((uint16_t*)((zl)+sizeof(uint32_t)*2))) // 获取ziplist的len指针
#define ZIPLIST_HEADER_SIZE (sizeof(uint32_t)*2+sizeof(uint16_t)) // ziplist头大小
#define ZIPLIST_END_SIZE (sizeof(uint8_t)) // ziplist结束标志位大小
#define ZIPLIST_ENTRY_HEAD(zl) ((zl)+ZIPLIST_HEADER_SIZE) // 获取第一个元素的指针
#define ZIPLIST_ENTRY_TAIL(zl) ((zl)+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))) // 获取最后一个元素的指针
#define ZIPLIST_ENTRY_END(zl) ((zl)+intrev32ifbe(ZIPLIST_BYTES(zl))-1) // 获取结束标志位指针
unsigned char *ziplistNew(void) { // 创建一个压缩表
unsigned int bytes = ZIPLIST_HEADER_SIZE+1; // zip头加结束标识位数
unsigned char *zl = zmalloc(bytes);
ZIPLIST_BYTES(zl) = intrev32ifbe(bytes); // 大小端转换
ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);
ZIPLIST_LENGTH(zl) = 0; // len赋值为0
zl[bytes-1] = ZIP_END; // 结束标志位赋值
return zl;
}
头信息
zlbytes :无符号32位整数,4字节,总字节数
zltail :无符号32位整数,4字节,尾节点的偏移量,列表中的最后一个节点到起始地址之间的字节数,如果知道压缩列表的起始地址加上zltail就是最后一个节点的起始地址,就能快速定位到列表的最后一个节点
zllen:无符号16位整数,2字节,entry节点的个数
结束标识 zlend:(16进制):0xff
节点 entry:占用字节数不确定,entry
一般来说连续内存空间每一个entry大小固定,便于根据脚标快速定位节点,但是存入压缩列表的数据占用大小会不一样,如果存了一个小值和大值,如果用同样的空间存储,就造成了资源浪费,为了节省内存,那么问题来了,那么entry长度怎么确定?怎么寻址遍历?
entry
typedef struct zlentry { // 压缩列表节点
unsigned int prevrawlensize, prevrawlen; // prevrawlen是前一个节点的长度,prevrawlensize是指prevrawlen的大小,有1字节和5字节两种
unsigned int lensize, len; // len为当前节点长度 lensize为编码len所需的字节大小
unsigned int headersize; // 当前节点的header大小
unsigned char encoding; // 节点的编码方式
unsigned char *p; // 指向节点的指针
} zlentry;
void zipEntry(unsigned char *p, zlentry *e) { // 根据节点指针返回一个enrty
ZIP_DECODE_PREVLEN(p, e->prevrawlensize, e->prevrawlen); // 获取prevlen的值和长度
ZIP_DECODE_LENGTH(p + e->prevrawlensize, e->encoding, e->lensize, e->len); // 获取当前节点的编码方式、长度等
e->headersize = e->prevrawlensize + e->lensize; // 头大小
e->p = p;
}
privious_entry_length,encoding
privious_entry_length:前一个节点的长度,占1个或5个字节
- 如果前一个节点的长度小于254,则采用1个字节来保存这个长度值
- 如果前一个节点的长度大于254,则采用5个字节来保存这个长度值,第一个字节固定为0xfe,后四个字节才是真实长度,描述长度也会占用内存,所以一般不推荐对单个数值占用过多内存
encoding:编码类型,第一部分记录content的数据类型(字符串还是整数)以及长度,数据类型的编码不同,所以在encoding里有个标识来标记采用哪个编码,第二部分记录content的长度,entry长度不固定指的是content的长度不固定,因此encoding要记录长度,而encoding的长度本身长度也是不固定的,占用1个、2个或5个字节(跟编码关系有关)
privious_entry_length,encoding长度都可以根据编码方式推算,真正变化的是content,而content长度记录在encoding里 ,因此entry的长度就知道了
entry总长度 = privious_entry_length字节数+encoding字节数+content字节数
为什么entry这么设计?
如果知道了当前的起始地址,因为entry是连续的,entry后一定是另一个entry,想知道下一个entry的地址,只要将当前的起始地址加上当前entry总长度。如果还想遍历下一个entry,只要继续同样的操作。
怎么倒序遍历?
当前节点的地址减去前一个节点的长度privious_entry_length,得到前一个节点的起始地址,只要重复此操作就可以实现倒序遍历。
因此不通过指针,通过记录长度也能实现正序,倒序遍历
记录长度的好处:占用内存小,1或者5个字节。
encoding怎么标记字符串还是整数?怎么记录长度的?
Encoding编码
encoding和privious_entry_length两部分采用的都是小端字节序进行存储的
小端字节序:从低位到高位保存
例如一个16进制数(两位16进制数从00到FF,表示0-255的10进制数,也就是8 位(1个字节)的数值形式。):0x1234,12是一个字节,34是一个字节,采用小端字节序存储值位:0x3412
字符串:如果encoding是以"01"、"01"、或者"10"开头,则证明content是字符串
编码 | 编码长度 | 字符串大小 |
---|---|---|
|00pppppp| 后6位记录长度 | 1 bytes | <= 63 bytes |
|01pppppp|qqqqqqqq| 14位记录长度 | 2 bytes | <= 16383 bytes |
|10000000|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt| 32位记录长度 | 5 bytes | <= 4294967295 bytes |
encoding记录一部分编码,一部分长度,content长度可长可短,如果content只占了5个字节,长度就是5,只用一个字节就能记录,如果content保存的字符串很大,要几万几百万字节,就需要更多字节记录长度。
例如:保存字符串: "ab"和"bc",
"ab"大小为2字节,采用第一种编码encoding为 00000010,为第一个节点,privious_entry_length为00000000,content是"ab"转化成字节97和98,01100001(64+32+1),01100010
"ab"转化为16进制保存
privious_entry_length:0x00 encoding:0x02 content:0x61 0x62
"bc":
privious_entry_length("ab"的长度4):0x04 encoding:0x02 content:0x62 0x63
也就是说ab字符串的entry为4个字节
我们也能推出整个ziplist的结构
整数:如果encoding是以“11”开始,则证明content是整数,且encoding固定只占用1个字节
字符串长度变动大,而整数最大8个字节,encoding记录长度只要一个字节
编码 | 编码长度 | 整数类型 |
---|---|---|
11000000 | 1 | int16_t(2 bytes) |
11010000 | 1 | int32_t(4 bytes) |
11100000 | 1 | int64_t(8 bytes) |
11110000 | 1 | 24位有符整数(3 bytes) |
11111110 | 1 | 8位有符整数(1 bytes) |
1111xxxx | 1 | 直接在xxxx位置保存数值,范围从0001~1101,减1后结果为实际值 |
如果有很小的如1、2,用1个字节来表示它,就有点浪费,因此有专门记录小的数字的编码 1111xxxx
用4bit位直接记录数值而不是长度,0000和1110和1111(结束标识)都被占用,范围从0001到1101。表示实际值0到12
例如:一个ziplist包含两个整数值:2 和 5
previous_entry_length:00000000 encoding: 11110011 不需要content,16进制表示entry: 0x00 0xf3
两个entry:0x00 0xf3 0x02 0xf6
整个ziplist:
使用限制:只能从前向后或者从后往前,如果长度很长,要找的数在中间,效率就很慢
连锁更新问题
如果有n个250~253字节之间的entry的ziplist,往头部插入254字节大小的entry,后一个entry记录前一个节点长度的privious_entry_length就会从原来的1个字节变为5个字节,因此这个entry变为了254个字节,同样的,又会导致后面的entry都变为entry
为什么要在元素较少的时候使用 ziplist ?
因为 redis 中的集合容器中,很多情况都用到了链表的实现,元素和元素之间通过储存的关联指针有序的串联起来,但是这样的指针往往是 随机I/O,也就是指针地址是不连续的(分布不均匀)。而我们的 ziplist 它本身是一块连续的内存块,所以它的读写是 顺序I/O,从底层的磁盘读写来说,顺序I/O 的效率肯定是高于 随机I/O 。你可能会问了,那为什么不都用 顺序I/O 的 ziplist 代替 随机I/O 呢,因为 ziplist 是连续内存,当你元素数量多了,意味着当你创建和扩展的时候需要操作更多的内存,所以 ziplist 针对元素少的时候才能提升效率。
Redis基本数据结构ZipList的更多相关文章
- 探索Redis设计与实现4:Redis内部数据结构详解——ziplist
本文转自互联网 本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial ...
- 【转】Redis内部数据结构详解——ziplist
本文是<Redis内部数据结构详解>系列的第四篇.在本文中,我们首先介绍一个新的Redis内部数据结构--ziplist,然后在文章后半部分我们会讨论一下在robj, dict和zipli ...
- 聊一聊Redis的数据结构
如果没有记错的话,应该是在两个月前把 我们经常看到此类的文章: Redis的五种数据结构 Redis的数据结构以及对应的使用场景 其实以数据结构这个词去说明Redis的String.Hash.List ...
- Redis学习笔记之Redis基本数据结构
Redis基础数据结构 Redis有5种基本数据结构:String(字符串).list(列表).set(集合).hash(哈希).zset(有序集合) 字符串string 字符串类型是Redis的va ...
- 你真的懂redis的数据结构了吗?redis内部数据结构和外部数据结构揭秘
Redis有哪些数据结构? 字符串String.字典Hash.列表List.集合Set.有序集合SortedSet. 很多人面试时都遇到过这种场景吧? 其实除了上面的几种常见数据结构,还需要加上数据结 ...
- [转]Redis内部数据结构详解-sds
本文是<Redis内部数据结构详解>系列的第二篇,讲述Redis中使用最多的一个基础数据结构:sds. 不管在哪门编程语言当中,字符串都几乎是使用最多的数据结构.sds正是在Redis中被 ...
- redis内部数据结构深入浅出
最大感受,无论从设计还是源码,Redis都尽量做到简单,其中运用到的原理也通俗易懂.特别是源码,简洁易读,真正做到clean and clear, 这篇文章以unstable分支的源码为基准,先从大体 ...
- redis内部数据结构和外部数据结构揭秘
Redis有哪些数据结构? 字符串String.字典Hash.列表List.集合Set.有序集合SortedSet. 很多人面试时都遇到过这种场景吧? 其实除了上面的几种常见数据结构,还需要加上数据结 ...
- 你真的懂了redis的数据结构吗?redis内部数据结构和外部数据结构揭秘
原文链接:https://mp.weixin.qq.com/s/hKpAxPE-9HJgV6GEdV4WoA Redis有哪些数据结构? 字符串String.字典Hash.列表List.集合Set.有 ...
随机推荐
- 详细图解 Netty Reactor 启动全流程 | 万字长文 | 多图预警
本系列Netty源码解析文章基于 4.1.56.Final版本 大家第一眼看到这幅流程图,是不是脑瓜子嗡嗡的呢? 大家先不要惊慌,问题不大,本文笔者的目的就是要让大家清晰的理解这幅流程图,从而深刻的理 ...
- Transferable Joint Attribute-Identity Deep Learning for Unsupervised Person Re-Identification理解
简介:这篇文章属于跨域无监督行人再识别,不同于大部分文章它使用了属性标注.旨在于能够学习到有属性语义与有区分力的身份特征的表达空间(TJ-AIDL),并能够转移到一个没有看到过的域. 贡献: 提出了一 ...
- 【docker专栏4】使用docker安装nginx提供web服务
一般学习一项技术,会先用一个最简单的例子或最典型的例子来向大家讲解入门内容,所以此文为大家介绍使用docker安装nginx容器服务.从基础使用的角度来讲,此文几乎涵盖了docker最核心的内容:镜像 ...
- android studio取消设置代理
看标题感觉就是一个简单的设置,其实只是个大坑啊 https://www.jianshu.com/p/bb6d2bcdd5b5 android studio内虽然设置了 no proxy,但是没起作用, ...
- 微信小程序使用echarts/数据刷新重新渲染/图层遮挡问题
1.微信小程序使用echarts,首先下载echarts并导入小程序项目中,因小程序后期上线对文件大小有要求,所以建议进行定制下载导入可减少文件大小占比,也可以下载以前旧版本文件比较小的应付使用 下载 ...
- SpringBoot数据库管理 - 用flyway对数据库管理和迁移
上文介绍了Liquibase,以及和SpringBoot的集成.除了Liquibase之外,还有一个组件Flyway也是经常被使用到的类似的数据库版本管理中间件.本文主要介绍Flyway, 以及Spr ...
- 类型转换_str()函数与int()函数
数据类型转换 需要将不同数据类型拼接在一起的时候就需要先进行数据类型转换 str+str//这里的+叫做连接字符,有点类似C++中的操作符重载,老对象里面的内容了 在python中整型和字符串类型不能 ...
- C#常见的集合
3中数组式的 Array 在内存上是连续分配的,而且元素类型是一样的 特点:读取快,可以坐标访问,增删慢.长度不变. ArrayList 不定长,连续分配的,元素没有类型限制,任何元素都当成Objec ...
- SpringBoot集成文件 - 如何使用POI导出Word文档?
前文我们介绍了通过Apache POI导出excel,而Apache POI包含是操作Office Open XML(OOXML)标准和微软的OLE 2复合文档格式(OLE2)的Java API.所以 ...
- 用 Windows Server 2019 搭建求生之路服务器
准备工作 要搭建一台 Windows Server 的求生之路服务器需要做以下几点前置工作: 购买一台云服务器,如腾讯云: 下载 SteamCMD: 安装 SourceMod.MateMod.L4dT ...