Redis Set 用了 2 种数据结构来存储,到现在才知道

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: Redis Set 用了 2 种数据结构来存储,到现在才知道

Sets 无序集合,他的功能就好像你熟悉的 Java 中的 HashSet 一样。集合是通过散列表实现的,所以添加、删除、查找元素的时间复杂度是 O(1)。

1. 是什么

Sets 是 String 类型的无序集合,集合中的元素是唯一的,集合中不会出现重复的数据

Java 的 HashSet 底层是用 HashMap 实现,Sets 的底层数据结构也是用 Hashtable(散列表)实现,散列表的 key 存的是 Sets 集合元素的 value,散列表的 value 则指向 NULL。

不同的是,当元素内容都是 64 位以内的十进制整数的时候,并且元素个数不超过 set-max-intset-entries 配置的值(默认 512)的时候,会使用更加省内存的 intset(整形数组)来存储

图2-15

使用场景

当你需要存储多个元素,并且要求不能出现重复数据,无需考虑元素的有序时,就可以使用 Sets 来存储,这样能利用我对单个元素操作 O(1) 时间复杂度带来的性能优势。

并且 Sets 还支持在集合之间做交集、并集、差集操作,比如当你遇到如下场景,需要统计多个集合元素的聚合结果。

  • 统计多个元素的共有数据(交集)。
  • 统计两个集合其中的一个独有元素(差集统计)。
  • 统计多个集合的所有元素(并集统计)。

常见的使用场景。

  1. 社交软件中共同关注,通过交集实现。
  2. 每日新增关注数,只需要对近两天的总注册用户量集合取差集即可。
  3. 打标签:比如微信收藏功能,你可以为自己收藏的每一篇文章打标签,这样你可以快速的找到被添加了某个标签的所有文章。

2. 修炼心法

关于散列表结构我会在专门的章节介绍,先看 intset 结构,结构体定义在源码 intset.h中。

typedef struct intset {
    uint32_t encoding;
    uint32_t length;
    int8_t contents[];
} intset;
  • length,记录整数集合存储的元素个数,其实就是 contents 数组的长度。
  • contents,真正存储整数集合的数组,是一块连续内存区域。每个元素都是数组的一个数组元素,数组中的元素会按照值的大小从小到大有序排列存储,并且不会有重复元素。
  • encoding,编码格式,决定数组类型,一共有三种不同的值。
  • INTSET_ENC_INT16,表示 contents 数组的存储元素是 int16_t 类型,每 2 字节表示一个整数元素。
  • INTSET_ENC_INT32,表示 contents 数组的存储元素是 int32_t 类型,每 4 字节表示一个元素。
  • INTSET_ENC_INT64,表示 contents 数组的存储元素是 int64_t 类型,每 8 字节表示一个元素。

图2-16

MySQL:“如果在一个 int16_t 类型的整数集合中插入一个 int64_t 类型的值会怎样?”

这个问题问得好,下次可以继续保持。

这种情况会触发整数集合升级,也就是集合的所有元素都会转换成 int64_t 类型,步骤如下。

  1. 根据新元素的类型,以及集合元素的数量,包括新添加的元素在内,计算新的空间大小,对底层数组空间扩容,进行空间重新分配。
  2. 将数组原有的元素都转换成新元素类型,把转换后的元素按照从大到小的顺序放到正确的位置上,需要保证数组元素的有序性
  3. 修改 encoding 的值,length + 1。

所以每次向整形数组集合添加新元素都可能会引起升级,升级又会对原始数据进行类型转换,时间复杂度是 O(N)。

MySQL:“如果删除刚刚添加的 int64_t 类型元素,会执行降级操作么?”

整形数组不支持降级操作。

MySQL:“Sets 是无序集合,为何存储整形数字的场景下 contents 数组元素需要有序?”

为了查询元素速度,数组有序我就能使用二分法来提高查询效率insetFind() 函数返回值等于 0 表示集合中没有目标数据,反之 1 存在目标数据。方法的内部会调用 intsetSearch() 函数使用二分法来实现。

static uint8_t intsetSearch(intset *is, int64_t value, uint32_t *pos) {
    int min = 0, max = intrev32ifbe(is->length)-1, mid = -1;
    int64_t cur = -1;
    // 省略一些检查代码
    while(max >= min) {
        mid = ((unsigned int)min + (unsigned int)max) >> 1;
        cur = _intsetGet(is,mid);
        if (value > cur) {
            min = mid+1;
        } else if (value < cur) {
            max = mid-1;
        } else {
            break;
        }
    }
 // 修改 pos 指针
    if (value == cur) {
        if (pos) *pos = mid;
        return 1;
    } else {
        if (pos) *pos = min;
        return 0;
    }
}

pos 指针的作用有两个,如果查找到目标值, pos 记录目标值的位置;查找不到目标值,pos 记录的就是这个目标值插入到 intset 的位置。

3. 出招实战:共同好友

三国天下有限公司开发了一个名叫“三国恋”的社交 APP,想要实现共同好友功能,这个场景就能使用集合交集来实现。为每个用户创建一个 Sets 集合,账号名作为集合的 key,集合 value 存储该账号的好友。

如下指令构建刘备和曹操的好友集合。

SADD user:刘备 赵子龙 张飞 关羽 貂蝉
SADD user:曹操 貂蝉 夏侯惇 典韦 张辽

想要知道两个人的共同好友,也就是两个集合的交集,只需要使用 SINTERSTORE指令。

SINTERSTORE user:曹刘好友 user:刘备 user:曹操

命令执行后,刘备与曹操两个集合的交集数据就存储到了“user:曹刘好友”集合中。使用 SMEMBERS 查看曹操与刘备的共同好友。

redis> SMEMBERS user:曹刘好友
1) "貂蝉"

好家伙,他们都喜欢貂蝉,你喜不喜欢呢?


关注我,提升技术能力


相关文章
|
1月前
|
消息中间件 缓存 NoSQL
Redis各类数据结构详细介绍及其在Go语言Gin框架下实践应用
这只是利用Go语言和Gin框架与Redis交互最基础部分展示;根据具体业务需求可能需要更复杂查询、事务处理或订阅发布功能实现更多高级特性应用场景。
194 86
|
23天前
|
存储 消息中间件 NoSQL
Redis数据结构:别小看这5把“瑞士军刀”,用好了性能飙升!
Redis提供5种基础数据结构及多种高级结构,如String、Hash、List、Set、ZSet,底层通过SDS、跳表等实现高效操作。灵活运用可解决缓存、计数、消息队列、排行榜等问题,结合Bitmap、HyperLogLog、GEO更可应对签到、UV统计、地理位置等场景,是高性能应用的核心利器。
|
1月前
|
存储 缓存 NoSQL
Redis基础命令与数据结构概览
Redis是一个功能强大的键值存储系统,提供了丰富的数据结构以及相应的操作命令来满足现代应用程序对于高速读写和灵活数据处理的需求。通过掌握这些基础命令,开发者能够高效地对Redis进行操作,实现数据存储和管理的高性能方案。
74 12
|
1月前
|
存储 消息中间件 NoSQL
【Redis】常用数据结构之List篇:从常用命令到典型使用场景
本文将系统探讨 Redis List 的核心特性、完整命令体系、底层存储实现以及典型实践场景,为读者构建从理论到应用的完整认知框架,助力开发者在实际业务中高效运用这一数据结构解决问题。
|
11月前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
908 9
|
11月前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
237 59
|
4月前
|
编译器 C语言 C++
栈区的非法访问导致的死循环(x64)
这段内容主要分析了一段C语言代码在VS2022中形成死循环的原因,涉及栈区内存布局和数组越界问题。代码中`arr[15]`越界访问,修改了变量`i`的值,导致`for`循环条件始终为真,形成死循环。原因是VS2022栈区从低地址到高地址分配内存,`arr`数组与`i`相邻,`arr[15]`恰好覆盖`i`的地址。而在VS2019中,栈区先分配高地址再分配低地址,因此相同代码表现不同。这说明编译器对栈区内存分配顺序的实现差异会导致程序行为不一致,需避免数组越界以确保代码健壮性。
75 0
栈区的非法访问导致的死循环(x64)
232.用栈实现队列,225. 用队列实现栈
在232题中,通过两个栈(`stIn`和`stOut`)模拟队列的先入先出(FIFO)行为。`push`操作将元素压入`stIn`,`pop`和`peek`操作则通过将`stIn`的元素转移到`stOut`来实现队列的顺序访问。 225题则是利用单个队列(`que`)模拟栈的后入先出(LIFO)特性。通过多次调整队列头部元素的位置,确保弹出顺序符合栈的要求。`top`操作直接返回队列尾部元素,`empty`判断队列是否为空。 两题均仅使用基础数据结构操作,展示了栈与队列之间的转换逻辑。
|
9月前
|
存储 C语言 C++
【C++数据结构——栈与队列】顺序栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现顺序栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 1.初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储
359 77
|
8月前
|
算法 调度 C++
STL——栈和队列和优先队列
通过以上对栈、队列和优先队列的详细解释和示例,希望能帮助读者更好地理解和应用这些重要的数据结构。
184 11
下一篇
oss教程