文章目录
介绍
在 Java 里面,HashSet 底层是用 HashMap 实现的。在 Redis 里面也是类似的,Redis 里面的 Hash 底层结构是 dict,Set 底层的结构也是 dict。但是,在元素都是整数值的时候,Set 可以用一种更省空间
的方式来存数据,这种省空间的方式就是这一节要说的 intset。
一个 Set 要用 intset 作为底层存储的话,需要满足两个条件:
- 元素都是整数类型
- 这个 Set 里面的元素个数,要少于
set-max-intset-entries
配置指定的这个值,这个值默认是 512
一旦这个 Set 集合不满足这两个条件,就会切换成 dict 作为底层存储。Redis 之所以使用 intset 结构来进行优化,主要是为了减少内存碎片,提高查询效率,这也体现了 Redis 在空间占用和耗时等方面的折中和思考。
关于dict, 可以参考这篇文章 认识redis的hash
intset 结构体
从名字就可以看出,intset 结构体是用来存储整数类型的集合,不仅如此,intset 中存储的整数还是有序的,这样我们就可以非常方便地使用二分查找来查找一个元素。下面来看 intset 结构体的定义:
typedef struct intset {
uint32_t encoding; // 编码类型
uint32_t length; // contents数组的长度
int8_t contents[]; // 柔性数组,不使用时不占用空间
} intset;
- encoding: 编码格式,表示 intset 中存储的每个元素需要用几个字节来存储。其可选值有如下三个:
- INTSET_ENC_INT16:值为 2,表示所有元素值都处于 [INT16_MIN, INT16_MAX] 范围内,那么在 contents 数组中每 2 个字节表示一个整数元素。
- INTSET_ENC_INT32:值为 4,表示有元素的值处于 (INT16_MAX, INT32_MAX] 或是 [INT32_MIN, INT16_MIN) 范围内,那么在 contents 数组中每 4 个字节表示一个元素
- INTSET_ENC_INT64:值为 8,表示有元素的值处于 (INT32_MAX, INT64_MAX] 或是 [INT64_MIN, INT32_MIN) 范围内,那么在 contents 数组中每 8 个字节表示一个元素。
- length 字段:表示 intset 中的元素个数
- contents 字段:用来存储 intset 中元素的数组,这是个柔性数组,不使用时不会占用空间
从 intset 结构体的定义可以看出,它与前文介绍的 ziplist 类似,也是通过一块连续的内存空间实现的,如下图所示,其中 encoding 和 length 两部分构成了 intset 的头,总共占 8 字节。
添加数据与扩容
- 计算插入值的编码值, 判断是不是在encoding范围内, 如果小于intset的encoding编码值, 数据会按照当前编码方式存储, 并占据当前编码大小的字节数, Redis 不会降级或压缩 intset 中的编码类型
- 当插入的值的编码值大于当前intset的encoding范围, 会触发升级
- 扩容时,将所有元素从当前编码类型转换为更大的编码类型; 16位 → 32位, 32位 → 64位
- 例如, 集合内存储 int16 类型的整数,范围为:-32768 ~ 32767, 当插入超过范围的数,例如 40000,会触发扩容,将整个集合升级为 int32 类型; 若再插入超过 int32 范围的数,例如 10^10,会再扩容为 int64 类型
- 数据重新排列,迁移到新的内存空间
查找数据
- 计算目标值的编码值, 判断是否在encoding范围内, 不存在直接查找失败
- 编码值匹配成功后, 使用二分法查找
总结
- 当set集合中的元素满足以下条件时会使用intset作为存储结构, 否则使用dict作为数据存储结构
- 元素都是整数类型
- 这个 Set 里面的元素个数,要少于
set-max-intset-entries
配置指定的这个值,这个值默认是 512
- intset根据不同的编码类型, 使用不同字节存储整数
- INTSET_ENC_INT16(2位): -215 ~ 215-1
- INTSET_ENC_INT32(4位): -232 ~ 232-1
- INTSET_ENC_INT64(8位): -264 ~ 264-1
- intset中的数据存储有序
个人公众号: 行云代码