`ziplist`的`head`和`end`存的都是长度和标记,而`entry`存储的是具体元素,这又是经过特殊设计的一种存储格式,每个`entry`都以包含两段信息的元数据作为前缀,每一个`entry`的组成结构为:
~~~properties
<prevlen> <encoding> <entry-data>
~~~
#### prevlen
`prevlen`属性存储了前一个`entry`的长度,通过此属性能够从后到前遍历列表。`prevlen`属性的长度可能是`1`字节也可能是`5`字节:
* 当链表的前一个`entry`占用字节数小于`254`,此时`prevlen`只用`1`个字节进行表示。
~~~c
<prevlen from 0 to 253> <encoding> <entry>
~~~
* 当链表的前一个`entry`占用字节数大于等于`254`,此时`prevlen`用`5`个字节来表示,其中第`1`个字节的值固定是`254`(相当于是一个标记,代表后面跟了一个更大的值),后面`4`个字节才是真正存储前一个`entry`的占用字节数。
~~~c
0xFE <4 bytes unsigned little endian prevlen> <encoding> <entry>
~~~
注意:`1`个字节你完全能存储`255`的大小,之所以只取到`254`是因为`zlend`就是固定的`255`,所以`255`这个数要用来判断是否是`ziplist`的结尾。
#### encoding
`encoding`属性存储了当前`entry`所保存数据的类型以及长度。`encoding`长度为`1`字节,`2`字节或者`5`字节长。前面我们提到,每一个`entry`中可以保存字节数组和整数,而`encoding`属性的第`1`个字节就是用来确定当前`entry`存储的是整数还是字节数组。当存储整数时,第`1`个字节的前两位总是`11`,而存储字节数组时,则可能是`00`、`01`和`10`三种中的一种。
* 当存储整数时,第`1`个字节的前`2`位固定为`11`,其它位则用来记录整数值的类型或者整数值(下表所示的编码中前两位均为`11`):
| 编码 | 长度 | entry 保存的数据 |
| --- | --- | --- |
| 11000000 | 1字节 | int16\_t 类型整数 |
| 11010000 | 1字节 | int32\_t 类型整数 |
| 11100000 | 1字节 | int64\_t 类型整数 |
| 11110000 | 1字节 | 24 位有符号整数 |
| 11111110 | 1字节 | 8 位有符号整数 |
| 1111xxxx | 1字节 | `xxxx`代表区间`0001-1101`,存储了一个介于`0-12`之间的整数,此时`entry-data`属性被省略 |
注意:`xxxx`四位编码范围是`0000-1111`,但是`0000`、`1111`和`1110`已经被表格中前面表示的数据类型占用了,所以实际上的范围是`0001-1101`,此时能保存数据`1-13`,再减去`1`之后范围就是`0-12`。至于为什么要减去`1`是从使用习惯来说,`0`是一个非常常用的数据,所以才会选择统一减去`1`来存储一个`0-12`的区间而不是直接存储`1-13`的区间。
* 当存储字节数组时,第`1`个字节的前`2`位为`00`、`01`或者`10`,其它位则用来记录字节数组的长度:
| 编码 | 长度 | entry保存的数据 |
| --- | --- | --- |
| 00pppppp | 1字节 | 长度小于等于`63`字节(`6`位)的字节数组 |
| 01pppppp qqqqqqqq | 2字节 | 长度小于等于`16383`字节(`14`位)的字节数组 |
| 10000000 qqqqqqqq rrrrrrrr ssssssss tttttttt | 5字节 | 长度小于等于`2`的`32`次方减`1`(`32`位)的字节数组,其中第`1`个字节的后`6`位设置为`0`,暂时没有用到,后面的`32`位(`4`个字节)存储了数据 |
#### entry-data
`entry-data`存储的是具体数据。当存储小整数(`0-12`)时,因为`encoding`就是数据本身,此时`entry-data`部分会被省略,省略了`entry-data`部分之后的`ziplist`中的`entry`结构如下:
~~~properties
<prevlen> <encoding>
~~~
压缩列表中`entry`的数据结构定义如下(源码`ziplist.c`文件内),当然实际存储并没有直接使用到这个结构定义,这个结构只是用来接收数据,所以大家了解一下就可以了:
~~~c
typedef struct zlentry {
unsigned int prevrawlensize;//存储prevrawlen所占用的字节数
unsigned int prevrawlen;//存储上一个链表节点需要的字节数
unsigned int lensize;//存储len所占用的字节数
unsigned int len;//存储链表当前节点的字节数
unsigned int headersize;//当前链表节点的头部大小(prevrawlensize + lensize)即非数据域的大小
unsigned char encoding;//编码方式
unsigned char *p;//指向当前节点的起始位置(因为列表内的数据也是一个字符串对象)
} zlentry;
~~~
- Redis 为什么这么快
- 什么是 Redis
- Redis 的安装
- Redis 到底有多快
- Redis 是单线程还是多线程
- Redis 为什么选择使用单线程来执行请求
- 什么是 IO 多路复用机制
- Redis 中 I/O 多路复用的应用
- 一个简单的字符串,为什么 Redis 要设计的如此特别
- Redis 的 9 种数据类型
- 二进制安全字符串
- sds 空间分配策略
- sds 和 C 语言字符串区别
- sds 是如何被存储的
- type 属性
- encoding 属性
- 通过牺牲速度来节省内存,Redis 是觉得自己太快了吗
- 什么是压缩列表
- ziplist 的存储结构
- entry 存储结构
- ziplist 数据示例
- ziplist 连锁更新问题
- 为了加快速度,Redis 都做了哪些“变态”设计
- 列表对象
- linkedlist
- linkedlist 和 ziplist 的选择
- quicklist
- 列表对象常用操作命令
- Redis 中哈希分布不均匀该怎么办
- 哈希对象
- hashtable
- ziplist
- ziplist 和 hashtable 的编码转换
- 哈希对象常用命令
- 同一份数据,Redis 为什么要存”两次”
- 五种基本类型之集合对象
- intset 编码
- 集合对象常用命令
- 五种基本类型之有序集合对象
- skiplist 编码
- ziplist 编码
- ziplist 和 skiplist 编码转换
- 有序集合对象常用命令
- 要想用活 Redis,Lua 脚本是绕不过去的坎
- 发布与订阅
- 基于频道的实现
- 基于模式的实现
- Lua 脚本
- Lua 脚本的调用
- Lua 脚本中执行 Redis 命令
- Lua 脚本摘要
- Lua 脚本文件
- 脚本异常
- 作为一款内存数据库,为什么断电后 Redis 数据不会丢失
- Redis 持久化机制
- RDB 持久化机制
- AOF 持久化机制
- 内存耗尽后 Redis 会发生什么
- 内存回收
- 过期策略
- 8 种淘汰策略
- LRU 算法
- LFU 算法
- 不能回滚的 Redis 事务还能用吗
- Redis 有事务吗
- Redis 事务实现原理
- Redis 事务 ACID 特性
- watch 命令
- watch 命令的作用
- watch 原理分析
- Redis 为什么不直接用 master-slave 集群
- Redis 集群方案
- 主从复制
- 配置一主两从 master-slave 集群
- 主从复制原理分析
- 主从服务的不足之处
- Sentinel(哨兵)机制为什么从神坛滑落
- 哨兵 Sentinel 机制
- Sentinel 原理分析
- 配置 Sentinel 集群
- Sentinel 机制实战
- Sentinel 机制的不足之处
- Redis Cluster 集群凭什么成为了最终的胜利者
- Redis 分布式集群方案
- 客户端实现分片
- 中间代理服务实现分片
- Redis Cluster 方案
- 手动配置一个 Redis Cluster 集群
- Redis Cluster 集群常用命令
- 客户端如何使用 Redis Cluster 集群
- Redis Cluster 的不足
- 如何从 10 亿数据中快速判断是否存在某一个元素
- 缓存雪崩
- 缓存击穿
- 缓存穿透
- 布隆过滤器(Bloom Filter)
- 布隆过滤器的 2 大特点
- 布隆过滤器的实现(Guava)
- 布隆过滤器的如何删除