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

飞算JavaAI代码审查落地难题:90%团队忽略的4个关键细节

第一章:飞算JavaAI代码合规检查概述 飞算JavaAI代码合规检查是一款面向Java开发者的智能化代码质量管控工具,深度融合静态代码分析与人工智能技术,旨在提升代码安全性、可维护性与规范性。该工具不仅支持常见的编码规范检测(如阿里巴巴Java开发手册),还能基于AI模型识别潜在的业务逻辑缺陷和安全漏洞。 核心功能特点 * 智能规则引擎:内置数百条行业标准规则,覆盖命名规范、异常处理、并发控制等关键维度 * AI辅助诊断:通过机器学习模型分析历史缺陷数据,预测高风险代码段 * 实时反馈机制:在IDE插件中实现编码过程中的即时提示,提升修复效率 * 企业级策略管理:支持自定义规则集,满足不同组织的合规要求 典型使用场景 场景说明代码提交前检查集成至Git预提交钩子,阻止不合规代码入库CI/CD流水线集成作为构建阶段的质量门禁,确保上线代码符合标准团队代码评审辅助自动生成评审报告,聚焦关键问题点 快速接入示例 以下为Maven项目中引入飞算JavaAI检查插件的基本配置: <build> <plugins> <!-- 飞算JavaAI合规检查插件 -->

By Ne0inhk

Spring AI

目录 基本概念 什么是 AI 模型(Model) 大语言模型  (LLM) 提示词 (Prompt) 词元(Token) Spring AI 是什么 快速入门 环境要求 申请 API Key 项目创建 接口编写 核心接口 ChatModel  ChatClient 消息类型 SystemMessage UserMessage AssistantMessage 输出格式 结构化输出 流式输出 SSE 协议介绍 SSE 数据格式 data event id retry SSE 使用示例 Flux Advisors 基本概念 什么是 AI AI:也就是 人工智能(

By Ne0inhk
把废弃的腾讯云服务器改为 Openclaw 仅需一句话!!!(附带免费白嫖AI模型)

把废弃的腾讯云服务器改为 Openclaw 仅需一句话!!!(附带免费白嫖AI模型)

大家好,我是热爱探索AI前沿技术的LucianaiB。 前面我尝试了,感兴趣的可以才是部署一下试试 1.在 Windows 上部署 Openclaw:https://mp.weixin.qq.com/s/iF3ED1e649kkmdR26Y1xiw 2.把 Openclaw 接入到 Moltbook:https://mp.weixin.qq.com/s/QUrB50iwRGGdkl1LO-Tl8Q 相信很多技术爱好者都有这样的经历:趁着双十一或者大促,脑子一热买了一台腾讯云或者阿里云的服务器。买的时候雄心勃勃,想着要搭建博客、跑脚本、做图床。结果呢?大概率是跑了几个自动化签到脚本后,它就静静地躺在控制台里“吃灰”,每个月白白扣费。 但是在自己的电脑运行 Openclaw 无法做到24小时的在运行,于是我就想到了我有一个好久不用的腾讯云服务器,之前购买主要是跑一些自动化签到脚本,并没有实际做什么具体工作。于是我就想到把废弃的腾讯云服务器改为 Openclaw 的24小时的服务器。 于是,

By Ne0inhk
Spring AI Tool Calling(工具调用)详解——让大模型拥有“动手能力“

Spring AI Tool Calling(工具调用)详解——让大模型拥有“动手能力“

定位:本文是 Spring AI 系列博客之一。我们将从为什么需要工具调用讲起,结合 Spring AI 官方文档和实战代码,一步步带你理解 Tool Calling 的原理、用法和进阶技巧。即使你是初学者,也能看懂。 希望对于大家学习Spring AI 有帮助 目录 * 1. 什么是 Tool Calling? * 2. 为什么需要 Tool Calling? * 3. Tool Calling 的工作原理 * 4. 快速入门:第一个工具 * 5. 定义工具的三种方式 * 6. 工具规范(Tool Specification) * 7. 使用工具的两种 API * 8. @ToolParam——告诉模型参数怎么填 * 9. ToolContext—

By Ne0inhk