为什么要有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的更多相关文章

  1. 探索Redis设计与实现4:Redis内部数据结构详解——ziplist

    本文转自互联网 本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial ...

  2. 【转】Redis内部数据结构详解——ziplist

    本文是<Redis内部数据结构详解>系列的第四篇.在本文中,我们首先介绍一个新的Redis内部数据结构--ziplist,然后在文章后半部分我们会讨论一下在robj, dict和zipli ...

  3. 聊一聊Redis的数据结构

    如果没有记错的话,应该是在两个月前把 我们经常看到此类的文章: Redis的五种数据结构 Redis的数据结构以及对应的使用场景 其实以数据结构这个词去说明Redis的String.Hash.List ...

  4. Redis学习笔记之Redis基本数据结构

    Redis基础数据结构 Redis有5种基本数据结构:String(字符串).list(列表).set(集合).hash(哈希).zset(有序集合) 字符串string 字符串类型是Redis的va ...

  5. 你真的懂redis的数据结构了吗?redis内部数据结构和外部数据结构揭秘

    Redis有哪些数据结构? 字符串String.字典Hash.列表List.集合Set.有序集合SortedSet. 很多人面试时都遇到过这种场景吧? 其实除了上面的几种常见数据结构,还需要加上数据结 ...

  6. [转]Redis内部数据结构详解-sds

    本文是<Redis内部数据结构详解>系列的第二篇,讲述Redis中使用最多的一个基础数据结构:sds. 不管在哪门编程语言当中,字符串都几乎是使用最多的数据结构.sds正是在Redis中被 ...

  7. redis内部数据结构深入浅出

    最大感受,无论从设计还是源码,Redis都尽量做到简单,其中运用到的原理也通俗易懂.特别是源码,简洁易读,真正做到clean and clear, 这篇文章以unstable分支的源码为基准,先从大体 ...

  8. redis内部数据结构和外部数据结构揭秘

    Redis有哪些数据结构? 字符串String.字典Hash.列表List.集合Set.有序集合SortedSet. 很多人面试时都遇到过这种场景吧? 其实除了上面的几种常见数据结构,还需要加上数据结 ...

  9. 你真的懂了redis的数据结构吗?redis内部数据结构和外部数据结构揭秘

    原文链接:https://mp.weixin.qq.com/s/hKpAxPE-9HJgV6GEdV4WoA Redis有哪些数据结构? 字符串String.字典Hash.列表List.集合Set.有 ...

随机推荐

  1. 面试官:Redis 过期删除策略和内存淘汰策略有什么区别?

    作者:小林coding 计算机八股文网站:https://xiaolincoding.com 大家好,我是小林. Redis 的「内存淘汰策略」和「过期删除策略」,很多小伙伴容易混淆,这两个机制虽然都 ...

  2. Halcon图片标定,使得后续图片处理过后变成与模板图片一样

    随便选择一张图片 对这张图片进行旋转矫正之后,图片就变成了一个模板图片.它的区域region位置如图所示:   当来了一张新的图片的时候,让它与region比较,与模板的位置有明显的偏差, 如图所示: ...

  3. ubuntu20.04安装测试uhttpd

    uhttpd是openwrt上运行一个高效小型Http服务,支持cgi, lua等特性.可以直接通过snap方式安装,如果是16.04,18.04或者20.04,snap已经默认安装了:如果是其它版本 ...

  4. python解决“failed to execute pyi_rth_pkgres”问题

    pip uninstall pyinstaller pip install https://github.com/pyinstaller/pyinstaller/archive/develop.zip

  5. CF1703A YES or YES? 题解

    题意:输入一个长度为 \(3\) 的字符串,判断其是否为 \(YES\),忽略大小写. 做法:输入字符串,直接判断. #include<cstdio> #include<iostre ...

  6. SpringBoot中Tomcat和SpringMVC整合源码分析

    概述 ​ SpringBoot中集成官方的第三方组件是通过在POM文件中添加组件的starter的Maven依赖来完成的.添加相关的Maven依赖之后,会引入具体的jar包,在SpringBoot启动 ...

  7. 第十一天python3 递归函数

    递归Recursion 函数直接或者间接调用自身就是递归: 递归需要有边界条件,递归前进段.递归返回段: 递归一定要有边界条件: 当边界条件不满足的时候,递归前进: 当边界条件满足的时候,递归返回: ...

  8. .netcore 定制化项目开发的思考和实现

    今年年初进了一家新公司,进入之后一边维护老项目一边了解项目流程,为了接下来的项目重做积累点经验. 先说下老项目吧,.net fx 3.5+oracle...... 在实际维护中逐渐发现,老项目有标准版 ...

  9. Javaweb-JSP详解

    一.什么是JSP Java Server Pages:Java服务器端页面,和Servlet一样,用于动态web技术 最大的特点: 写JSP就像在写HTML 区别: HTML只给用户提供静态的数据 J ...

  10. for(int i=0;i<1000;i++)与 for(int i=1;i<=1000;i++)。 if ( i%500){}//前者表示0-501一个循环,后者1-500一个循环

    `package com.Itbz; import java.sql.Connection; import java.sql.PreparedStatement; /** 向mysql数据库批量添加数 ...