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.有 ...
随机推荐
- STC8H开发(十二): I2C驱动AT24C08,AT24C32系列EEPROM存储
目录 STC8H开发(一): 在Keil5中配置和使用FwLib_STC8封装库(图文详解) STC8H开发(二): 在Linux VSCode中配置和使用FwLib_STC8封装库(图文详解) ST ...
- PoweJob高级特性-MapReduce完整示例
由于网上搜索 PowerJob MapReduce 都是设计原理,demo也展示个空壳子,没有演示Map到Reduce结果怎么传递,对于没有MR开发经验的人来说并没有什么帮助,所以这里写了一个有完整计 ...
- 树莓派实战:微信机器人(itchat实现)
背景 楼主有一台树莓派4B开发板(8G内存版),是目前的顶配机型.这一年来的业余时间,除了写Java.架构方面的文章,也陆续折腾了不少树莓派上的好玩小项目,在此新开一个树莓派实战的文章系列,分享给粉丝 ...
- 记录一下第一次在CodeForces供题的事(未完待续)
3月11日 因为想出题而开始打比赛上分 (Rating 1727) (期间最低掉到 1669) 6月4日凌晨 上分,有了权限 (Rating 2141) 6月4-6日 出了七道题 6月8-12日 又出 ...
- 升级了Springboot版本后项目启动不了了
问题背景 项目上使用的springboot版本是2.1.1.RELEASE,现在因为要接入elasticsearch7.x版本,参考官方文档要求,需要将springboot版本升级到2.5.14. 本 ...
- linux学习(小白篇)
当前服务器:centos 7 shell命令框:xshell 文件预览及上传:xftp (界面化软件,非常好用) 数据库连接:navicat 此文是在学习linux时做一个指令合集,方便自己查阅 进文 ...
- JAVA基础-11-Java Number 类--九五小庞
问题:一直有疑惑,为什么java中学习了基本数据类型,而不使用,使用的是封装的对象. 解答: 一般地,当需要使用数字的时候,我们通常使用内置数据类型,如:byte.int.long.double 等. ...
- 面试突击73:IoC 和 DI 有什么区别?
IoC 和 DI 都是 Spring 框架中的重要概念,就像玫瑰花与爱情一样,IoC 和 DI 通常情况下也是成对出现的.那 IoC 和 DI 什么关系和区别呢?接下来,我们一起来看. 1.IoC 介 ...
- Redis 17 缓存穿透 缓存击穿 缓存雪崩
参考源 https://www.bilibili.com/video/BV1S54y1R7SB?spm_id_from=333.999.0.0 版本 本文章基于 Redis 6.2.6 使用缓存的问题 ...
- UI自动化框架搭建之Python3
UI自动化框架搭建--unittest 使用的代码是Python3版本,与时俱进哈哈 解释一下我的框架目录接口(每个人框架的目录接口不一样,根据实际要求) common目录:公共模块,这个地方可以存放 ...