Redis 解锁:C++ 实战深度探索 Set 数据类型

Redis 解锁:C++ 实战深度探索 Set 数据类型

前言

欢迎来到 Redis Set 的终极指南。如果您曾需要管理一组独一无二的元素集合——无论是用户 ID、文章标签还是邮件地址——并希望以闪电般的速度对其执行强大的集合运算,那么您来对地方了。Redis Set 绝不是一个简单的列表,它是一种精妙的数据结构,将数学中强大的集合理论直接带入您的高性能数据库中。

在本文中,我们将从最基础的概念讲起,逐步深入到高级的实际应用。我们将使用优秀的 C++ 库 redis-plus-plus 来演示所有示例,并逐行剖析代码。无论您是 C++ 开发者、后端工程师,还是仅仅对 Redis 感到好奇,读完本文,您都将深刻理解是什么让 Set 成为 Redis 中功能最丰富的工具之一。

Redis Set 究竟是什么?

在我们深入代码之前,先来建立一个清晰的思维模型。想象你有一个魔力袋,你可以往里面扔东西,但这个袋子有两条非常特殊的规则:

  1. 强制保持唯一:这个袋子会自动拒绝重复的物品。如果你想把一个标有“A”的弹珠放进一个已经有“A”弹珠的袋子里,它会阻止你,确保袋子里每样东西都只有一个。
  2. 顺序毫不在意:当你从袋子里往外取东西时,它们的顺序是完全随机的。袋子不记得到底是按什么顺序把东西放进去的。

这个“魔力袋”正是 Redis Set 的精准比喻:一个无序的、元素唯一的字符串集合。这个简单的定义是其强大功能的基石,使其能够以惊人的速度进行成员资格检查、数量统计以及诸如交集、并集等复杂的服务器端运算。


第一章:基础入门 - 创建和查看你的第一个 Set

让我们从最基本的操作开始:如何向一个 Set 添加元素,以及如何查看它的全部内容。为此,我们将使用 SADDSMEMBERS 这两个命令。

SADD:向集合中添加成员

SADD 是您向 Set 中添加一个或多个元素的主要工具。如果某个元素已经存在,Redis 会优雅地忽略它。该命令的返回值是成功添加的元素的数量。

SMEMBERS:获取所有成员

SMEMBERS 的功能正如其名:返回指定 Set 中的所有成员。这对于获取整个集合非常有用,但请注意:在拥有数百万元素的超大 Set 上使用此命令可能会暂时阻塞您的 Redis 服务器,因为它需要时间来准备所有数据。我们将在后续章节中讨论更安全的替代方案 SSCAN

C++ 实战:saddsmembers

现在,让我们来分析一段代码,它演示了这些基础操作。

image.png
// 引入必要的头文件...#include<iostream>#include<set>#include<string>#include<vector>#include<iterator>#include<sw/redis++/redis.h>// 一个辅助函数,用于打印容器内容template<typenameT>voidPrintContainer(const T& container){for(constauto& elem : container){ std::cout << elem <<" ";} std::cout << std::endl;}voidtest1(sw::redis::Redis& redis){ std::cout <<"sadd 和 smembers"<< std::endl;// 清空数据库,确保一个干净的测试环境 redis.flushall();// 1. 一次添加一个元素 redis.sadd("key","111");// 2. 使用初始化列表,一次添加多个元素 redis.sadd("key",{"222","333","444"});// 3. 使用迭代器,从另一个容器中添加多个元素 std::set<std::string> elems ={"555","666","777"};// 返回值是成功插入了多少个元素 redis.sadd("key", elems.begin(), elems.end());// --- 现在,让我们获取所有元素 --- std::set<std::string> result;// 为我们的 C++ set 构建一个插入迭代器auto it = std::inserter(result, result.end());// 从 Redis set 中获取所有成员,并插入到我们的 C++ set 中 redis.smembers("key", it);PrintContainer(result);}
代码剖析:
  1. redis.flushall():我们首先清空整个 Redis 数据库,以确保测试环境的纯净。
  2. 单个元素 saddredis.sadd("key", "111"); 将字符串 “111” 添加到名为 key 的 Set 中。由于 Set 原本是空的,此命令返回 1
  3. 初始化列表 saddredis.sadd("key", {"222", "333", "444"}); 展示了 redis-plus-plus 库的一个便捷特性,允许您一次性添加多个元素。这比发送三个独立的命令效率更高。此调用将返回 3
  4. 基于迭代器的 sadd:在这里,我们先填充了一个 C++ 的 std::set,然后使用它的迭代器(elems.begin(), elems.end())将其所有元素添加到 Redis 的 Set 中。这对于将现有 C++ 容器中的数据同步到 Redis 非常有用。
  5. 使用 smembers 获取数据
    • 我们创建了一个 std::set<string> result; 来存放从 Redis 返回的数据。在客户端使用 std::set 是一个绝佳选择,因为它不仅 mirroring(镜像)了 Redis Set 的唯一性,还能自动对元素进行排序,便于我们进行可预测的展示。
    • auto it = std::inserter(result, result.end()); 是至关重要的一行。我们需要一种方式告诉 redis-plus-plus 应该把接收到的元素放在哪里inserter 是一种特殊的迭代器,当你给它赋值时,它会调用其关联容器的 insert() 方法。
    • redis.smembers("key", it); 执行命令。redis-plus-plus 获取 key 中的所有成员,并使用我们的迭代器 it 将它们逐一插入到 result 集合中。
C++ 关键概念:inserter vs back_inserter

在原始笔记中,有一个关键的区别被强调了出来:

  • std::back_inserter 创建一个调用 push_back() 的迭代器。它适用于 std::vector, std::list, std::deque 等容器。
  • std::set没有push_back() 方法,因为它需要维护内部的排序。因此,对于 std::set,我们必须使用 std::inserter,它会调用 insert() 方法。
预测输出:

PrintContainer 函数将打印 result 集合的内容。由于 std::set 会对其元素进行排序,输出将是按字母/数字顺序排列的。

sadd 和 smembers 111 222 333 444 555 666 777 

第二章:深入探索 - 检查与修改你的 Set

既然我们知道了如何构建一个 Set,接下来让我们学习如何查询它的属性并执行基本的修改。这些命令是 Set 日常操作的核心,并且它们都快得令人难以置信。

SISMEMBER:这个元素存在吗? (时间复杂度 O(1))

这是 Set 命令库中最强大的命令之一。SISMEMBER 检查一个特定元素是否是 Set 的成员。如果存在,返回 1 (true);如果不存在,返回 0 (false)。它的性能是 O(1),这意味着其速度是恒定的,不依赖于 Set 的大小。无论是在一个有10个元素的 Set 还是在一个有1000万个元素的 Set 中检查成员资格,花费的时间都是相同的。

C++ 实战:sismember
image.png
voidtest2(sw::redis::Redis& redis){ std::cout <<"sismember"<< std::endl; redis.flushall(); redis.sadd("key",{"111","222","333","444"});// 检查 "111" 是否是集合的成员bool result = redis.sismember("key","111"); std::cout <<"result:"<< result << std::endl;}
  • 剖析:我们创建一个 Set,然后使用 sismember 检查 “111” 是否存在。redis-plus-plus 库非常方便地将 Redis 返回的 10 直接映射为了 C++ 的 bool 类型。因为 “111” 确实在 Set 中,result 将为 true
  • 应用场景
    • 标签系统:检查一篇博客文章是否已经被标记为 “DevOps”。
    • 权限控制:检查一个 userID 是否在 admin_users 这个 Set 中。
    • 唯一性事件:检查用户是否已经执行了某个一次性操作(例如,“voted_on_poll_123”)。
  • 预测输出:当 bool true 被输出到 cout 时,通常会显示为 1
sismember result:1 

SCARD:集合里有多少元素? (时间复杂度 O(1))

SCARD 代表 “Set Cardinality”(集合基数),它简单地返回一个 Set 中元素的数量。与 SISMEMBER 一样,这也是一个 O(1) 操作。Redis 内部维护了一个计数器,所以它不需要遍历所有元素就能告诉你总数。

C++ 实战:scard
image.png
voidtest3(sw::redis::Redis& redis){ std::cout <<"scard"<< std::endl; redis.flushall();// 向集合中添加4个唯一元素 redis.sadd("key",{"111","222","333","444"});// 获取集合中的元素个数longlong result = redis.scard("key");// 返回 4 std::cout <<"result:"<< result << std::endl;}
  • 剖析:我们添加了四个元素,然后调用 scard。命令返回了计数 4
  • 应用场景
    • 在线用户:跟踪已登录的独立用户数量。
    • 点赞计数:快速显示一张照片获得的独立点赞数。
    • 数据分析:统计今天访问网站的独立 IP 地址数量。
  • 预测输出
scard result:4 

SPOP:随机移除并返回一个元素

SPOP 是一个既有趣又实用的命令。它会从 Set 中随机选择一个元素,将其移除,然后返回给你。这是一种“破坏性读取”,因为元素在被读取后就从集合中消失了。

C++ 实战:spop
image.png
voidtest4(sw::redis::Redis& redis){ std::cout <<"spop"<< std::endl; redis.flushall(); redis.sadd("key",{"111","222","333","444"});// 随机弹出一个元素,spop 的返回值是 Optional<string>auto result = redis.spop("key");if(result){// 因为返回值是 Optional,我们通过 .value() 来获取原始的 string 内容 std::cout <<"result:"<< result.value()<< std::endl;}else{ std::cout <<"result is empty"<< std::endl;}}
  • 剖析
    • auto result = redis.spop("key"); 执行命令。
    • redis-plus-plus 将返回值包装在 sw::redis::Optional<std::string> 中。这是因为如果你对一个空 Set 执行 spop,Redis 会返回 nil(空)。Optional 类型可以优雅地处理这种情况,避免空指针等问题。
    • if (result) 检查 Optional 对象是否真的包含一个值。在我们的例子中,由于 Set 非空,它肯定会弹出一个元素,所以条件为真。
    • result.value()Optional 中提取出实际的 std::string 值。
  • 核心特性:随机性SPOP 最大的特点就是随机。这意味着每次运行这段代码,得到的结果都可能不同。它非常适合需要随机处理任务的场景。
  • 应用场景
    • 抽奖系统:从参与用户 Set 中随机抽取一名中奖者。
    • 任务队列:从待处理任务池中随机分配一个任务给工作进程。
    • 在线匹配:从等待匹配的玩家池中随机抽取一个进行游戏。
  • 预测输出:输出是不确定的,可能是以下四种情况之一:
// 可能的输出 1 spop result:111 // 可能的输出 2 spop result:333 

第三章:集合的威力 - 集合运算

这才是 Redis Set 真正大放异彩的地方。Redis 能够在服务器端以极高的效率执行集合的交集 (intersection)并集 (union)差集 (difference) 运算,避免了将大量数据传输到客户端再进行计算的开销。

交集运算:SINTER & SINTERSTORE

交集运算会找出所有给定的 Set 中共同存在的元素。

  • SINTER: 计算交集并直接返回给客户端。
  • SINTERSTORE: 计算交集,但不返回,而是将结果存储在一个新的目标 Set 中
C++ 实战:sinter (求交集并返回)
image.png
voidtest5(sw::redis::Redis& redis){// 这里的 cout 应该是 "sinter",一个小笔误 std::cout <<"sinter"<< std::endl; redis.flushall(); redis.sadd("key1",{"111","222","333","444"}); redis.sadd("key2",{"111","222","444"}); std::set<std::string> result;auto it = std::inserter(result, result.end());// 求交集涉及多个 key,我们使用初始化列表来描述// 将 "key1" 和 "key2" 的交集插入到 result 中 redis.sinter({"key1","key2"}, it);PrintContainer(result);}
  • 剖析
    • key1 包含 {"111", "222", "333", "444"}
    • key2 包含 {"111", "222", "444"}
    • redis.sinter({"key1", "key2"}, it); 命令计算出两个集合的共同成员是 {"111", "222", "444"},并通过迭代器将它们存入 C++ 的 result 集合中。
  • 应用场景
    • 共同好友:计算用户A的好友列表和用户B的好友列表的交集。
    • 内容推荐:找出同时对 “科幻” 和 “悬疑” 标签感兴趣的用户。
  • 预测输出
sinter 111 222 444 
C++ 实战:sinterstore (求交集并存储)
image.png
voidtest6(sw::redis::Redis& redis){ std::cout <<"sinterstore"<< std::endl; redis.flushall(); redis.sadd("key1",{"111","222","333"}); redis.sadd("key2",{"111","222","444"});// 指定一个 destination ("key3"),将交集结果存储到其中longlong len = redis.sinterstore("key3",{"key1","key2"}); std::cout <<"len:"<< len << std::endl;// 检查 "key3" 中的元素以验证结果 std::set<std::string> result;auto it = std::inserter(result, result.end()); redis.smembers("key3", it);PrintContainer(result);}
  • 剖析
    • redis.sinterstore("key3", {"key1", "key2"}); 计算出交集 {"111", "222"},然后将这个结果存入一个全新的 Set key3 中。如果 key3 已存在,它将被覆盖。
    • 该命令返回新生成的 key3 集合的元素数量,即 2。所以 len 的值为 2
    • 后续的 smembers 验证了 key3 的内容确实是正确的交集结果。
  • 应用场景:当你需要缓存复用交集计算结果时,SINTERSTORE 非常有用。例如,为一组用户预先计算出他们共同喜欢的商品列表。
  • 预测输出
sinterstore len:2 111 222 

第四章:超越基础 - 更多强大的 Set 命令

我们已经覆盖了所提供代码中的所有命令,但 Redis Set 的能力远不止于此。为了成为真正的 Set 大师,让我们来了解一下其他一些极其有用的命令。

并集运算:SUNION & SUNIONSTORE

并集运算返回所有给定集合的全部不重复的元素。

  • 命令SUNION key [key ...]SUNIONSTORE destination key [key ...]
  • 应用场景
    • 好友圈:获取用户A的好友、用户B的好友和用户C的好友的完整、不重复的列表。
    • 权限合并:一个用户属于 “editor” 角色组和 “publisher” 角色组,通过并集可以得到该用户拥有的所有权限的集合。

差集运算:SDIFF & SDIFFSTORE

差集运算返回那些只存在于第一个集合中,但不在任何后续集合中的元素。

  • 命令SDIFF key [key ...]SDIFFSTORE destination key [key ...]
  • 应用场景
    • 好友推荐:找出我的好友中,有哪些还不是我朋友A的好友,从而可以向我推荐。
    • 内容去重:向用户展示新闻时,从“今日热点”中排除掉他“已读新闻”Set 中的内容。

安全迭代:SSCAN

正如前文提到的,SMEMBERS 对于大集合是危险的。SSCAN 提供了安全的替代方案。它使用一个游标 (cursor) 来分批次地返回集合中的元素,每次只返回一小部分,绝不会阻塞服务器。

  • 命令SSCAN key cursor [MATCH pattern] [COUNT count]
  • 工作方式:你用一个初始为 0 的游标开始第一次调用。Redis 返回下一批元素和一个新的游标。你用这个新的游标进行下一次调用,如此往复,直到返回的游标为 0,表示迭代完成。
  • 适用场景:任何需要遍历生产环境中大集合的操作,例如数据迁移、离线分析等。

总结

Redis Set 是一种看似简单却异常强大的数据结构。让我们回顾一下它的核心优势:

  • 唯一性:自动处理数据去重,简化了应用逻辑。
  • 极速性能:绝大多数核心操作(增、删、查、计数)的时间复杂度都是 O(1),性能与集合大小无关。
  • 强大的集合运算:能够在服务器端原子性地、高效地执行交、并、差集运算,极大地减少了网络开销和客户端的计算压力。

从简单的在线用户统计,到复杂的社交网络好友关系分析,再到智能推荐系统,Redis Set 都能以其优雅和高效提供坚实的解决方案。希望通过本文的深度解析和 C++ 代码示例,您已经准备好在自己的项目中发挥 Redis Set 的真正威力了。

Read more

Flutter 三方库 http_helper 的鸿蒙化适配指南 - 打造标准化的 REST 客户端封装、支持响应式异常拦截与请求全流程钩子

Flutter 三方库 http_helper 的鸿蒙化适配指南 - 打造标准化的 REST 客户端封装、支持响应式异常拦截与请求全流程钩子

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 http_helper 的鸿蒙化适配指南 - 打造标准化的 REST 客户端封装、支持响应式异常拦截与请求全流程钩子 前言 在 Flutter for OpenHarmony 的网络层开发中,直接使用底层的 http 库往往会导致大量的模板代码,且在处理拦截器、错误码统一转换和 Loading 态管理时力不从心。http_helper 是一套轻量级但功能完备的 REST 客户端封装库。它能帮助鸿蒙开发者快速构建一套符合工程化标准的服务层代码。本文将指导大家如何利用该库提升鸿蒙应用的网络交互质量。 一、原理解析 / 概念介绍 1.1 基础原理 http_helper 基于 Dart 的 http 包进行二次封装。它通过引入 Interceptor、

By Ne0inhk
Flutter 三方库 serial_csv 的鸿蒙化适配指南 - 实现极速的流式 CSV 数据编解码、支持端侧超大规模表格数据的高效序列化实战

Flutter 三方库 serial_csv 的鸿蒙化适配指南 - 实现极速的流式 CSV 数据编解码、支持端侧超大规模表格数据的高效序列化实战

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 serial_csv 的鸿蒙化适配指南 - 实现极速的流式 CSV 数据编解码、支持端侧超大规模表格数据的高效序列化实战 前言 在进行 Flutter for OpenHarmony 的金融报表、工业数据采集或大型列表导出应用开发时,CSV(Comma-Separated Values)由于其通用的文本属性成为首选的数据交换格式。然而,当文件达到数万行甚至更庞大时,常规的字符串拼接会导致内存爆炸。serial_csv 是一款专为极致性能设计的流式解析库。本文将探讨如何在鸿蒙端构建稳健、低开销的大数据处理方案。 一、原原理性解析 / 概念介绍 1.1 基础原理 serial_csv 采用了一种“增量扫描(Incremental Scanning)”算法。在读取时,它不一次性将整个文件加载进内存,而是通过缓冲区轮询,

By Ne0inhk
Flutter for OpenHarmony:debounce_throttle 防抖与节流的艺术(优化用户交互与网络请求) 深度解析与鸿蒙适配指南

Flutter for OpenHarmony:debounce_throttle 防抖与节流的艺术(优化用户交互与网络请求) 深度解析与鸿蒙适配指南

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net 前言 在 UI 交互开发中,有两种极其常见的性能问题: 1. 用户狂点按钮:导致发起了 10 次重复的表单提交请求。 2. 搜索框实时搜索:用户每输入一个字母就触发一次 API 搜索,浪费流量且导致界面闪烁。 解决这两个问题的标准答案是:防抖 (Debounce) 和 节流 (Throttle)。 虽然我们可以自己写 Timer 来实现,但处理 Timer 的销毁、重启逻辑很容易出错(造成内存泄漏或 NPE)。 debounce_throttle 库提供了封装良好、线程安全的防抖节流工具类,专为 Dart/Flutter 设计。 对于 OpenHarmony 应用,合理使用这些策略能显著降低 CPU 占用,

By Ne0inhk
Flutter 组件 slug 的适配 鸿蒙Harmony 实战 - 驾驭文本语义规范化、实现鸿蒙端中英混合标题转规范化文件名与 URL 路径方案

Flutter 组件 slug 的适配 鸿蒙Harmony 实战 - 驾驭文本语义规范化、实现鸿蒙端中英混合标题转规范化文件名与 URL 路径方案

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 slug 的适配 鸿蒙Harmony 实战 - 驾驭文本语义规范化、实现鸿蒙端中英混合标题转规范化文件名与 URL 路径方案 前言 在鸿蒙(OpenHarmony)生态的电商产品展示、博客文章发布以及分布式文件存储系统的开发中,如何处理具备高度随机性、包含特殊字符甚至是多语言混合的“文本标题”是一个常见的工程痛点。面对用户输入的 鸿蒙 0307 批次:跨平台实战! 这种长标题。如果直接将其作为文件名保存,可能会因为文件系统对特殊符号(如冒号、感叹号)的限制导致报错;如果将其作为 URL 路径,则会产生由于繁琐的百分比编码(URL Encoding)导致的地址不可读问题。 我们需要一种“语义透明、路径友好”的转码艺术。 slug 是一套专注于将杂乱文本转化为极致精简、规范化短链(

By Ne0inhk