蜻蜓点水说说Redis的ziplist的奥秘
本篇博客参考:
上篇博客中,我给大家蜻蜓点水般的介绍了Redis中SDS的奥秘,说明Redis之所以那么快,还有一个很重要、但是经常被大家忽视的一点,那就是Redis精心设计的数据结构。本篇博客,还是继续这个话题,给大家介绍下Redis另外一种底层数据结构:ziplist。
在Redis中,有五种基本数据类型,除了上篇博客提到的String,还有list,hash,zset,set,其中list,hash,zset都间接或者直接使用了ziplist,所以说理解ziplist也是相当重要的。
ziplist是什么意思
我刚开始看ziplist的时候,总觉得zip这个单词甚是熟悉,好像在日常使用电脑的时候经常看到,于是我百度了下:

哦哦,怪不得那么熟悉,原来就是“压缩”的意思,那ziplist就可以翻译成“压缩列表”了。
为什么要有ziplist
有两点原因:
- 普通的双向链表,会有两个指针,在存储数据很小的情况下,我们存储的实际数据的大小可能还没有指针占用的内存大,是不是有点得不偿失?而且Redis是基于内存的,而且是常驻内存的,内存是弥足珍贵的,所以Redis的开发者们肯定要使出浑身解数优化占用内存,于是,ziplist出现了。
- 链表在内存中,一般是不连续的,遍历相对比较慢,而ziplist可以很好的解决这个问题。
来看看ziplist的存在
zadd programmings 1.0 go 2.0 python 3.0 java
创建了一个zset,里面有三个元素,然后看下它采用的数据结构:
debug object programmings
"Value at:0x7f404ac30c60 refcount:1 encoding:ziplist serializedlength:36 lru:2689815 lru_seconds_idle:9"
HSET website google "www.g.cn
创建了一个hash,只有一个元素,看下它采用的数据结构:
debug object website
"Value at:0x7f404ac30ac0 refcount:1 encoding:ziplist serializedlength:30 lru:2690274 lru_seconds_idle:14"
可以很清楚的看到,zset和hash都采用了ziplist数据结构。
当满足一定的条件,zset和hash就不再使用ziplist数据结构了:

debug object website
"Value at:0x7f404ac30ac0 refcount:1 encoding:hashtable serializedlength:180 lru:2690810 lru_seconds_idle:2"
可以看到,hash的底层数据结构变成了hashtable。
szet就不做实验了,感兴趣的小伙伴们可以自己实验下。
至于这个转换条件是什么,放到后面再说。
好奇的你们,肯定会尝试看下list的底层数据结构是什么,发现并不是ziplist:
LPUSH languages python
debug object languages
"Value at:0x7f404c4763d0 refcount:1 encoding:quicklist serializedlength:21 lru:2691722 lru_seconds_idle:22 ql_nodes:1 ql_avg_node:1.00 ql_ziplist_max:-2 ql_compressed:0 ql_uncompressed_size:19"
可以看到,list采用的底层数据结构是quicklist,并不是ziplist。
在低版本的Redis中,list采用的底层数据结构是ziplist+linkedList,高版本的Redis中,quicklist替换了ziplist+linkedList,而quicklist也用到了ziplist,所以可以说list间接使用了ziplist数据结构。这个quicklist是什么,不是本篇博客的内容,暂且不表。
探究ziplist
ziplist源码:ziplist源码
ziplist源码的注释写的非常清楚,如果英语比较好,可以直接看上面的注释,如果你英语不是太好,或者没有一定的钻研精神,还是看看我写的博客吧。
ziplist布局
<zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend>
这是在注释中说明的ziplist布局,我们一个个来看,这些字段是什么:
- zlbytes:32bit无符号整数,表示ziplist占用的字节总数(包括本身占用的4个字节);
- zltail:32bit无符号整数,记录最后一个entry的偏移量,方便快速定位到最后一个entry;
- zllen:16bit无符号整数,记录entry的个数;
- entry:存储的若干个元素,可以为字节数组或者整数;
- zlend:ziplist最后一个字节,是一个结束的标记位,值固定为255。
Redis通过以下宏定义实现了对ziplist各个字段的存取:
// 假设char *zl 指向ziplist首地址
// 指向zlbytes字段
#define ZIPLIST_BYTES(zl) (*((uint32_t*)(zl)))
// 指向zltail字段(zl+4)
#define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t*)((zl)+sizeof(uint32_t))))
// 指向zllen字段(zl+(4*2))
#define ZIPLIST_LENGTH(zl) (*((uint16_t*)((zl)+sizeof(uint32_t)*2)))
// 指向ziplist中尾元素的首地址
#define ZIPLIST_ENTRY_TAIL(zl) ((zl)+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl)))
// 指向zlend字段,指恒为255(0xFF)
#define ZIPLIST_ENTRY_END(zl) ((zl)+intrev32ifbe(ZIPLIST_BYTES(zl))-1)
entry的构成
从ziplist布局中,我们可以很清楚的知道,我们的数据被保存在ziplist中的一个个entry中,我们下面来看看entry的构成。
<prevlen> <encoding> <entry-data>
我们再来看看这三个字段是什么:
- prevlen:前一个元素的字节长度,便于快速找到前一个元素的首地址,假如当前元素的首地址是x,那么(x-prevlen)就是前一个元素的首地址。
- encoding:当前元素的编码,这个字段实在是太复杂了,我们放到后面再说;
- entry-data:实际存储的数据。
prevlen
prevlen字段是变长的:
- 前一个元素的长度小于254字节时,prevlen用1个字节表示;
- 前一个元素的长度大于等于254字节时,prevlen用5个字节进行表示,此时,prevlen的第一个字节是固定的254(0xFE)(作为这种情况的一个标志),后面4个字节才表示前一个元素的长度。
encoding
下面就要介绍下encoding这个字段了,在此之前,大家可以到阳台吹吹风,喝口热水,再做个深呼吸,最后再做一个心理准备,因为这个字段实在是太复杂了,搞不好,看的时候,一下子吐了。。。如果实在无法理解,直接略过这一段吧。
Redis为了节约空间,对encoding字段进行了相当复杂的设计,Redis通过encoding来判断存储数据的类型,下面我们就来看看Redis是如何根据encoding来判断存储数据的类型的:
00xxxxxx最大长度位 63 的短字符串,后面的6个位存储字符串的位数;01xxxxxx xxxxxxxx中等长度的字符串,后面14个位来表示字符串的长度;10000000 aaaaaaaa bbbbbbbb cccccccc dddddddd特大字符串,需要使用额外 4 个字节来表示长度。第一个字节前缀是10,剩余 6 位没有使用,统一置为零;11000000表示 int16;11010000表示 int32;11100000表示 int64;11110000表示 int24;11111110表示 int8;11111111表示 ziplist 的结束,也就是 zlend 的值 0xFF;1111xxxx表示极小整数,xxxx 的范围只能是 (0001~1101), 也就是1~13。
如果是第10种情况,那么entry的构成就发生变化了:
<prevlen> <encoding>
因为数据已经存储在encoding字段中了。
可以看出Redis根据encoding字段的前两位来判断存储的数据是字符串(字节数组)还是整型,如果是字符串,还可以通过encoding字段的前两位来判断字符串的长度;如果是整形,则要通过后面的位来判断具体长度。
entry的结构体
我们上面说了那么多关于entry的点点滴滴,下面将要说的内容可能会颠覆你三观,我们在源码中可以看到entry的结构体,上面有一个注释非常重要:
/* We use this function to receive information about a ziplist entry.
* Note that this is not how the data is actually encoded, is just what we
* get filled by a function in order to operate more easily. */
typedef struct zlentry {
unsigned int prevrawlensize; /* Bytes used to encode the previous entry len*/
unsigned int prevrawlen; /* Previous entry len. */
unsigned int lensize; /* Bytes used to encode this entry type/len.
For example strings have a 1, 2 or 5 bytes
header. Integers always use a single byte.*/
unsigned int len; /* Bytes used to represent the actual entry.
For strings this is just the string length
while for integers it is 1, 2, 3, 4, 8 or
0 (for 4 bit immediate) depending on the
number range. */
unsigned int headersize; /* prevrawlensize + lensize. */
unsigned char encoding; /* Set to ZIP_STR_* or ZIP_INT_* depending on
the entry encoding. However for 4 bits
immediate integers this can assume a range
of values and must be range-checked. */
unsigned char *p; /* Pointer to the very start of the entry, that
is, this points to prev-entry-len field. */
} zlentry;
重点看上面的注释。一句话解释:这个结构体虽然定义出来了,但是没有被使用,因为如果真的这么使用的话,那么entry占用的内存就太大了。
ziplist的存储形式
Redis并没有像上篇博客介绍的SDS一样,封装一个结构体来保存ziplist,而是通过定义一系列宏来对数据进行操作,也就是说ziplist是一堆字节数据,上面所说的ziplist的布局和ziplist中的entry的布局只是抽象出来的概念。
为什么不能一直是ziplist
在文章比较前面的部分,我们做了实验来证明,满足一定的条件后,zset、hash的底层存储结构不再是ziplist,既然ziplist那么牛逼,Redis的开发者也花了那么多精力在ziplist的设计上面,为什么zset、hash的底层存储结构不能一直是ziplist呢?
因为ziplist是紧凑存储,没有冗余空间,意味着新插入元素,就需要扩展内存,这就分为两种情况:
- 分配新的内存,将原数据拷贝到新内存;
- 扩展原有内存。
所以ziplist 不适合存储大型字符串,存储的元素也不宜过多。
ziplist存储界限
那么满足什么条件后,zset、hash的底层存储结构不再是ziplist呢?在配置文件中可以进行设置:
hash-max-ziplist-entries 512 # hash 的元素个数超过 512 就必须用标准结构存储
hash-max-ziplist-value 64 # hash 的任意元素的 key/value 的长度超过 64 就必须用标准结构存储
zset-max-ziplist-entries 128 # zset 的元素个数超过 128 就必须用标准结构存储
zset-max-ziplist-value 64 # zset 的任意元素的长度超过 64 就必须用标准结构存储
对于这个配置,我只是一个搬运工,并没有去实验,毕竟没有人会去修改这个吧,感兴趣的小伙伴可以试验下。
看到了吧,Redis真不是想象中的那么简单,需要研究的东西还是挺多,也挺复杂的,如果我们不去学习,可能觉得自己完全掌握了Redis,但是一旦开始学习了,才发现我们先前掌握的只是皮毛。验证了一句话,知道的越多,不知道的越多。
本篇博客到这里就结束了。
蜻蜓点水说说Redis的ziplist的奥秘的更多相关文章
- 蜻蜓点水说说Redis的String的奥秘
本篇博客参考:掘金Redis小册 敖丙 如果面试官问你,单线程的Redis为什么那么快,你可能脱口而出,因为单线程,避免上下文切换:因为基于内存,比硬盘读写快很多:因为采用的是多路复用网络模型.不管你 ...
- Redis之ziplist数据结构
0.前言 redis初始创建hash表,有序集合,链表时, 存储结构采用一种ziplist的存储结构, 这种结构内存排列更紧密, 能提高访存性能. 本文介绍ziplist数据结构 1.ziplist存 ...
- Redis之ziplist源码分析
一.ziplist简介 从上一篇分析我们知道quicklist的底层存储使用了ziplist(压缩列表),由于压缩列表本身也有不少内容,所以重新开了一篇,在正式源码之前,还是先看下ziplist的特点 ...
- 【Redis】ziplist压缩列表
压缩列表 压缩列表是列表和哈希表的底层实现之一: 如果一个列表只有少量数据,并且数据类型是整数或者比较短的字符串,redis底层就会使用压缩列表实现. 如果一个哈希表只有少量键值对,并且每个键值对的键 ...
- 深入理解Redis:底层数据结构
简介 redis[1]是一个key-value存储系统.和Memcached类似,它支持存储的value类型相对更多,包括string(字符串).list(链表).set(集合).zset(sorte ...
- Redis源码分析系列
0.前言 Redis目前热门NoSQL内存数据库,代码量不是很大,本系列是本人阅读Redis源码时记录的笔记,由于时间仓促和水平有限,文中难免会有错误之处,欢迎读者指出,共同学习进步,本文使用的Red ...
- Redis 优化查询性能
一次使用 Redis 优化查询性能的实践 应用背景 有一个应用需要上传一组ID到服务器来查询这些ID所对应的数据,数据库中存储的数据量是7千万,每次上传的ID数量一般都是几百至上千数量级别. 以前 ...
- 一次使用 Redis 优化查询性能的实践
因为我的个人网站 restran.net 已经启用,博客园的内容已经不再更新.请访问我的个人网站获取这篇文章的最新内容,一次使用 Redis 优化查询性能的实践 应用背景 有一个应用需要上传一组ID到 ...
- Redis Scan迭代器遍历操作原理(一)
Redis在2.8.0版本新增了众望所归的scan操作,从此再也不用担心敲入了keys*, 然后举起双手看着键盘等待漫长的系统卡死了··· 命令的官方介绍在这里, 中文版由huangz同学细心翻译了, ...
随机推荐
- java 基本语法(十) 数组(三) 二维数组
1.如何理解二维数组? 数组属于引用数据类型数组的元素也可以是引用数据类型一个一维数组A的元素如果还是一个一维数组类型的,则,此数组A称为二维数组. 2.二维数组的声明与初始化 正确的方式: int[ ...
- JS中this指向的更改
JS中this指向的更改 JavaScript 中 this 的指向问题 前面已经总结过,但在实际开中, 很多场景都需要改变 this 的指向. 现在我们讨论更改 this 指向的问题. call更改 ...
- ETag简介与作用
ETag简介与作用 一.ETag简介 ETag是URL的tag,用来标示URL对象是否改变.这样可以应用于客户端的缓存:服务器产生ETag,并在HTTP响应头中将其传送到客户端,服务器用它来判断页面是 ...
- nginx 信号管理
本内容只针对nginx 关闭操作罗列方法技巧,不废话直接写,Nginx的信号控制如下: 1. TERM, INT 强制关闭进程 查看nginx进程ps -aux|grep nginx root 8 ...
- js 或Jquery操作定位元素
属性过滤常用javascript后去DOM对象 id是定位到的是单个element元素对象,其它的都是elements返回的是list对象 1.通过id获取 document.getElementBy ...
- git的分支远程连接和远程分支的拉取推送及冲突处理
目录 备注: 知识点 Feature分支 多人协作 推送分支 远程分支推送建议 克隆(clone)远程仓库 分支的推送和冲突处理 关联本地分支和远程分支 推送时指定分支或设置分支跟踪 拉取分支时文件冲 ...
- finalize()和四种引用的一点思考
一次对ThreadLocal的学习引发的思考 ThreadLocal对Entry的引用是弱引用,于是联想到四种引用的生命周期. 强引用,不会进行垃圾回收 软引用,JVM内存不够,进行回收 弱引用,下次 ...
- 解决android studio Gradle无法同步问题
打开根目录build.gradle buildscript { repositories { // 添加阿里云 maven 地址 maven { url 'http://maven.aliyun.co ...
- js的传递方式
回头过来复习一下. 从一个变量向另一个变量复制的时候,复制过去以后,都是单独独立的变量,当你改变其中一个的时候,并不会影响另一个变量.他们只是value相同而已: var a = 1; var b= ...
- Mosquitto的搭建(websocket、ssl、auth-plug)及坑点总结
Mosquitto的搭建及坑点总结 主要讲述的是eclipse-mosquitto的C语言版本的搭建,主要是为了从1.4.15版本升级到1.6.9,为解决一些webSocket和数据格式问题. 因为根 ...