Redis(二). 内存映射数据结构
上一章节介绍的几个数据机构功能强大,但是缺点是比较消耗内存,也就是内存利用率不高,为了解决这个问题,Redis 设计了内存映射数据结构来在一定的情况下代替内部数据结构;
内存映射数据结构特殊编码的字节序列,内存相比内部数据结构少很多,可以节约大量内存,但是缺点就是需要CPU 资源去按照规则解析这些字节序列;
1. 整数集合
intset 有序去重的保存整数值;根据大小,自动调整什么长度的整数类型来保存元素;
比如 在一个intset,保存的是int16_t 类型,新元素插入,新元素的类型是int32_t,intset 就会自动进行“升级”,所有元素升级为int32_t
1.1 应用
Redis 就会使用 intset 来保存集合元素 场景是:如果一个集合:
-
只保存着整数元素;
-
元素的数量不多
1.2 实现
intset 类型的定义
typedef struct intset {
// 保存元素所使用的类型的长度
uint32_t encoding;
// 元素个数
uint32_t length;
// 保存元素的数组
int8_t contents[];
} intset;
encoding 可以是下面其中之一 注意 encoding 使用 INTSET_ENC_INT16 作为初始值。
#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))
1.3 运行过程
API 添加新元素(升级) intsetUpgradeAndAdd
添加新元素到 intset 的工作由 intset.c/intsetAdd 函数完成,它需要处理以下三种情况:
-
元素已存在于集合,不做动作;
-
元素不存在于集合,并且添加新元素并不需要升级;
-
元素不存在于集合,但是要在升级之后,才能添加新元素;
并且,intsetAdd 需要维持 intset->contents 的以下性质:
-
确保数组中没有重复元素;
-
确保数组中的元素按从小到大排序;
插入过程
intset *is = intsetNew();
intsetAdd(is, 10, NULL);
// is->encoding = INTSET_ENC_INT16;
// is->length = 1;
// is->contents = [10];
intsetAdd(is, 5, NULL);
// is->encoding = INTSET_ENC_INT16;
// is->length = 2;
// is->contents = [5, 10];
##### 升级
intsetAdd(is, 65535, NULL);
// is->encoding = INTSET_ENC_INT32; // 升级
// is->length = 3;
// is->contents = [5, 10, 65535]; // 所有值使用 int32_t 保存
intsetAdd(is, 4294967295, NULL);
// is->encoding = INTSET_ENC_INT64; // 升级
// is->length = 4;
// is->contents = [5, 10, 65535, 4294967295];
升级实例 :升级的过程就是程序控制content 字节序列的位数挪动达到每个元素占用一样的位长
1.4 总结
• Intset 用于有序、无重复,根据元素的值,自动选择该用什么长度的整数类型。
• 长度更长的整数值添加到 intset 时,需要对 intset 进行升级,新 intset 中每个元素的位长度都等于新添加值的位长度,但原有元素的值不变。
• 升级会引起整个 intset 进行内存重分配,并移动集合中的所有元素,这个操作的复杂度为 O(N)
• Intset 只支持升级,不支持降级。
• Intset 是有序的,程序使用二分查找算法来实现查找操作,复杂度为 O(lg N) 。
2.压缩列表
Ziplist 是由一系列特殊编码的内存块构成的列表,一个 Ziplist 可以包含多个节点(entry),每个节点可以保存一个长度受限的字符数组(不以 \0 结尾的 char 数组)或者整数,包括:
• 字符数组
• 整数
2.1 实现
ziplist
参数 | 长度 | 值含义 |
---|---|---|
zlbytes | uint32_t | zlbytes总字节,内存重新分配时使用 |
zltail | uint32_t | 尾结点偏移量 |
zllen | uint16_t | ziplist 中节点的数量 |
entryX | ? | 每个节点长度不一致 都加起来的长度 |
zlend | uint8_t | 255 的二进制值 1111 1111 表示结束 |
ziplist 内的 entry
参数 | 长度 | 值含义 |
---|---|---|
pre_entry_length | 1/5 byte | 上个节点的字节长度,用于跳转到上一个节点, 前一个小于254 就使用 1byte保存 前一个大于254 就5 byte 第一个设置为254 后四byte保存长度 |
encoding | 2bit | 00 、01 和 10,11 |
length | ? | ziplist 中节点的数量 |
content | ? | 每个节点长度不一致 都加起来的长度 |
encoding + length
encoding 00 2bit 加上 length 6 bit (63) == 》 1 byte ==》content 长度小于等于 63 字节的字符数组
encoding 01 2bit 加上 length 14 bit (16383) == 》 2 byte==》content 长度小于等于 4294967295字节的字符数组
encoding 10_ _____ 2bit 加上 length 32 bit (4294967295) == 》 5 byte==》content 长度小于等于 4294967295字节的字符数组
11 比较复杂
编码 | 长度 | 保存的值 |
---|---|---|
11000000 | 1 byte | int16_t |
11010000 | 1 byte | int32_t |
11100000 | 1 byte | int64_t |
11110000 | 1 byte | 24 bit 有符号整数 |
11111110 | 1 byte | 8 bit 有符号整数 |
1111xxxx | 1 byte | 4 bit 无符号整数,介于 0 至 12 之间 |
示例 hello world
保存整数10086
2.2 创建ziplist
创建一个新的ziplist
2.3 添加元素到末端
- 定位 ziplist 的末端
- 程序需要计算新节点所需的空间
- 更新新节点的各项属性
2.4 将节点添加到某个**/**某些节点的前面
- next 的 pre_entry_length 域的长度正好能够编码 new 的长度(都是 1 字节或者都是 5
字节)
-
next 的 pre_entry_length 只有 1 字节长,但编码 new 的长度需要 5 字节
-
next 的 pre_entry_length 有 5 字节长,但编码 new 的长度只需要 1 字节
主要就是检测pre_entry_length ,因为这个不是固定长度导致的 长度
程序必须沿着路径一个个检查后续的节点是否满足新长度的编码要求,直到遇到一个能满足要求的节点
2.5 删除节点
- 定位目标节点,并计算节点的空间长度
- 进行内存移位,覆盖 target 原本的数据,然后通过内存重分配,收缩多余空间
- 检查 next 、next+1 等后续节点能否满足新前驱节点的编码,和添加操作一样,删除操作也可能会引起连锁更新。
2.6 遍历
可以对 ziplist 进行从前向后的遍历,或者从后先前的遍历;直接通过前面说元素内部占用位数大小直接加减既可以指针移动遍历数据
2.7 总结
- ziplist 是由一系列特殊编码的内存块构成的列表,它可以保存字符数组或整数值,它还是哈希键、列表键和有序集合键的底层实现之一。
- 添加和删除 ziplist 节点有可能会引起连锁更新 ,主要就是pre_entry_length ,因为这个不是固定长度导致的后面每一个entry都需要校验前一个;(可能+4byte,可能-4Byte)