概述
Redis Set 绝不是一个简单的列表,它是一种精妙的数据结构,将数学中强大的集合理论直接带入您的高性能数据库中。在本文中,我们将从最基础的概念讲起,逐步深入到高级的实际应用。我们将使用优秀的 C++ 库 来演示所有示例,并逐行剖析代码。
介绍 Redis Set 数据结构及其在 C++ 中的应用。通过 redis-plus-plus 库演示了 SADD、SMEMBERS、SISMEMBER、SCARD、SPOP 等基础命令,以及 SINTER、SUNION、SDIFF 等集合运算。重点讲解了 Set 的唯一性、无序性及 O(1) 时间复杂度特性,提供了完整的代码示例和场景分析。

Redis Set 绝不是一个简单的列表,它是一种精妙的数据结构,将数学中强大的集合理论直接带入您的高性能数据库中。在本文中,我们将从最基础的概念讲起,逐步深入到高级的实际应用。我们将使用优秀的 C++ 库 来演示所有示例,并逐行剖析代码。
redis-plus-plus想象你有一个魔力袋,你可以往里面扔东西,但这个袋子有两条非常特殊的规则:
这个'魔力袋'正是 Redis Set 的精准比喻:一个无序的、元素唯一的字符串集合。这个简单的定义是其强大功能的基石,使其能够以惊人的速度进行成员资格检查、数量统计以及诸如交集、并集等复杂的服务器端运算。
让我们从最基本的操作开始:如何向一个 Set 添加元素,以及如何查看它的全部内容。为此,我们将使用 SADD 和 SMEMBERS 这两个命令。
SADD 是您向 Set 中添加一个或多个元素的主要工具。如果某个元素已经存在,Redis 会优雅地忽略它。该命令的返回值是新成功添加的元素的数量。
SMEMBERS 的功能正如其名:返回指定 Set 中的所有成员。这对于获取整个集合非常有用,但请注意:在拥有数百万元素的超大 Set 上使用此命令可能会暂时阻塞您的 Redis 服务器,因为它需要时间来准备所有数据。我们将在后续章节中讨论更安全的替代方案 SSCAN。
#include <iostream>
#include <set>
#include <string>
#include <vector>
#include <iterator>
#include <sw/redis++/redis.h>
// 一个辅助函数,用于打印容器内容
template<typename T>
void PrintContainer(const T& container) {
for (const auto& elem : container) {
std::cout << elem << " ";
}
std::cout << std::endl;
}
void test1(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);
}
redis.flushall():我们首先清空整个 Redis 数据库,以确保测试环境的纯净。sadd:redis.sadd("key", "111"); 将字符串 '111' 添加到名为 key 的 Set 中。由于 Set 原本是空的,此命令返回 1。sadd:redis.sadd("key", {"222", "333", "444"}); 展示了 redis-plus-plus 库的一个便捷特性,允许您一次性添加多个元素。这比发送三个独立的命令效率更高。此调用将返回 3。sadd:在这里,我们先填充了一个 C++ 的 std::set,然后使用它的迭代器(elems.begin(), elems.end())将其所有元素添加到 Redis 的 Set 中。这对于将现有 C++ 容器中的数据同步到 Redis 非常有用。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 集合中。inserter vs back_inserterstd::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 检查一个特定元素是否是 Set 的成员。如果存在,返回 1 (true);如果不存在,返回 0 (false)。它的性能是 O(1),这意味着其速度是恒定的,不依赖于 Set 的大小。
void test2(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;
}
sismember 检查 '111' 是否存在。redis-plus-plus 库非常方便地将 Redis 返回的 1 或 0 直接映射为了 C++ 的 bool 类型。因为 '111' 确实在 Set 中,result 将为 true。userID 是否在 admin_users 这个 Set 中。bool true 被输出到 cout 时,通常会显示为 1。sismember result:1
SCARD 代表 'Set Cardinality'(集合基数),它简单地返回一个 Set 中元素的数量。与 SISMEMBER 一样,这也是一个 O(1) 操作。Redis 内部维护了一个计数器,所以它不需要遍历所有元素就能告诉你总数。
void test3(sw::redis::Redis& redis) {
std::cout << "scard" << std::endl;
redis.flushall();
// 向集合中添加 4 个唯一元素
redis.sadd("key", {"111", "222", "333", "444"});
// 获取集合中的元素个数
long long result = redis.scard("key");
// 返回 4
std::cout << "result:" << result << std::endl;
}
scard。命令返回了计数 4。scard result:4
SPOP 是一个既有趣又实用的命令。它会从 Set 中随机选择一个元素,将其移除,然后返回给你。这是一种'破坏性读取',因为元素在被读取后就从集合中消失了。
void test4(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 最大的特点就是随机。这意味着每次运行这段代码,得到的结果都可能不同。它非常适合需要随机处理任务的场景。// 可能的输出 1
spop result:111
// 可能的输出 2
spop result:333
这才是 Redis Set 真正大放异彩的地方。Redis 能够在服务器端以极高的效率执行集合的交集 (intersection)、并集 (union) 和差集 (difference) 运算,避免了将大量数据传输到客户端再进行计算的开销。
交集运算会找出所有给定的 Set 中共同存在的元素。
SINTER: 计算交集并直接返回给客户端。SINTERSTORE: 计算交集,但不返回,而是将结果存储在一个新的目标 Set 中。void test5(sw::redis::Redis& redis) {
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 集合中。sinter 111 222 444
void test6(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"),将交集结果存储到其中
long long 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
我们已经覆盖了所提供代码中的所有命令,但 Redis Set 的能力远不止于此。为了成为真正的 Set 大师,让我们来了解一下其他一些极其有用的命令。
并集运算返回所有给定集合的全部不重复的元素。
SUNION key [key ...] 和 SUNIONSTORE destination key [key ...]差集运算返回那些只存在于第一个集合中,但不在任何后续集合中的元素。
SDIFF key [key ...] 和 SDIFFSTORE destination key [key ...]正如前文提到的,SMEMBERS 对于大集合是危险的。SSCAN 提供了安全的替代方案。它使用一个游标 (cursor) 来分批次地返回集合中的元素,每次只返回一小部分,绝不会阻塞服务器。
SSCAN key cursor [MATCH pattern] [COUNT count]0 的游标开始第一次调用。Redis 返回下一批元素和一个新的游标。你用这个新的游标进行下一次调用,如此往复,直到返回的游标为 0,表示迭代完成。Redis Set 是一种看似简单却异常强大的数据结构。让我们回顾一下它的核心优势:
从简单的在线用户统计,到复杂的社交网络好友关系分析,再到智能推荐系统,Redis Set 都能以其优雅和高效提供坚实的解决方案。希望通过本文的深度解析和 C++ 代码示例,您已经准备好在自己的项目中发挥 Redis Set 的真正威力了。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online