Redis集合操作:SADD/SREM、SMEMBERS、SINTER/SUNION/SDIFF、SCARD、SPOP
集合(Set)在Redis里是个好东西。我做了这么多年嵌入式数据库,说实话,集合是我用得最多的数据结构之一。为什么?因为它天生就是干「去重」、「交集」、「并集」这些脏活累活的料。今天咱们就把Redis集合的常用操作捋一遍,从基础到实战,一步到位。
1. 集合是什么?先搞清楚概念
Redis的集合,说白了就是一个无序的、元素唯一的字符串集合。你往里面加重复元素?不好意思,它自动忽略。这个特性在去重场景下特别香。
我习惯把集合比作一个「装了不同颜色球的袋子」——你往里扔红球、蓝球、绿球,袋子里的球颜色都不重复。你想知道袋子里有没有红球?O(1)时间复杂度就查到了。这就是集合的威力。
核心特性:
- 元素唯一(自动去重)
- 无序(别指望顺序)
- 支持集合运算(交集、并集、差集)
- 单元素操作O(1),集合运算O(N)
2. 基础操作:SADD 和 SREM
先来最常用的两个命令——加元素和删元素。
SADD:往集合里加元素
// C语言中使用 hiredis 操作 Redis 集合
#include <hiredis/hiredis.h>
redisContext *c = redisConnect("127.0.0.1", 6379);
redisReply *reply;
// 添加单个元素
reply = redisCommand(c, "SADD myset element1");
printf("添加结果: %lld\n", reply->integer); // 返回1表示新增,0表示已存在
freeReplyObject(reply);
// 批量添加多个元素
reply = redisCommand(c, "SADD myset element2 element3 element4");
printf("新增元素数量: %lld\n", reply->integer);
freeReplyObject(reply);
嗯,这里要注意:SADD返回的是实际新增的元素个数。如果元素已经存在,它不会报错,只是返回0。我在项目中遇到过有人拿这个返回值做判断,结果踩了坑——以为返回0就是操作失败,其实只是元素已存在而已。
SREM:从集合里移除元素
// 移除元素
reply = redisCommand(c, "SREM myset element1");
printf("移除结果: %lld\n", reply->integer); // 返回1表示成功移除,0表示元素不存在
freeReplyObject(reply);
// 批量移除
reply = redisCommand(c, "SREM myset element2 element3");
printf("移除元素数量: %lld\n", reply->integer);
freeReplyObject(reply);
个人经验:我习惯在删除前先检查元素是否存在,虽然SREM本身不会报错,但明确知道操作结果能避免后续逻辑混乱。尤其是做数据同步时,删除不存在的元素可能意味着数据状态不一致。
3. 查看集合:SMEMBERS 和 SCARD
SMEMBERS:获取集合所有元素
// 获取集合所有成员
reply = redisCommand(c, "SMEMBERS myset");
if (reply->type == REDIS_REPLY_ARRAY) {
printf("集合元素数量: %zu\n", reply->elements);
for (size_t i = 0; i < reply->elements; i++) {
printf("元素[%zu]: %s\n", i, reply->element[i]->str);
}
}
freeReplyObject(reply);
这里有个坑——SMEMBERS返回的元素顺序是不确定的。你想想看,集合本身就是无序的,所以别指望两次调用返回的顺序一样。我曾经在调试时发现两次打印结果顺序不同,还以为是bug,后来才反应过来是集合的特性。
SCARD:获取集合元素个数
// 获取集合大小
reply = redisCommand(c, "SCARD myset");
printf("集合元素个数: %lld\n", reply->integer);
freeReplyObject(reply);
SCARD是O(1)操作,Redis内部维护了集合的计数器。所以哪怕集合里有上百万个元素,SCARD也是一瞬间的事。
避坑指南:我曾经在项目中用SMEMBERS获取一个包含10万元素的集合,结果客户端直接卡死了。为什么?因为SMEMBERS会把所有元素一次性返回,网络传输和内存开销都很大。如果集合很大,建议用SSCAN做游标遍历,别用SMEMBERS。
4. 集合运算:SINTER、SUNION、SDIFF
这才是集合的杀手锏。三个集合运算能解决很多业务问题。
SINTER:交集运算
// 计算两个集合的交集
reply = redisCommand(c, "SADD set1 a b c d");
freeReplyObject(reply);
reply = redisCommand(c, "SADD set2 c d e f");
freeReplyObject(reply);
reply = redisCommand(c, "SINTER set1 set2");
printf("交集结果: ");
for (size_t i = 0; i < reply->elements; i++) {
printf("%s ", reply->element[i]->str);
}
printf("\n"); // 输出: c d
freeReplyObject(reply);
SUNION:并集运算
// 计算两个集合的并集
reply = redisCommand(c, "SUNION set1 set2");
printf("并集结果: ");
for (size_t i = 0; i < reply->elements; i++) {
printf("%s ", reply->element[i]->str);
}
printf("\n"); // 输出: a b c d e f
freeReplyObject(reply);
SDIFF:差集运算
// 计算set1相对于set2的差集(在set1中但不在set2中)
reply = redisCommand(c, "SDIFF set1 set2");
printf("差集结果: ");
for (size_t i = 0; i < reply->elements; i++) {
printf("%s ", reply->element[i]->str);
}
printf("\n"); // 输出: a b
freeReplyObject(reply);
实战场景:我在做用户标签系统时,就用集合运算实现了「共同关注」、「可能认识的人」等功能。比如用户A关注了{a,b,c},用户B关注了{c,d,e},那么SINTER就是共同关注,SUNION就是所有关注的人,SDIFF就是A关注了但B没关注的。
5. 随机操作:SPOP
SPOP是从集合中随机弹出一个或多个元素。这个命令在抽奖、随机推荐等场景下特别好用。
// 随机弹出一个元素
reply = redisCommand(c, "SPOP myset");
if (reply->type == REDIS_REPLY_STRING) {
printf("随机弹出的元素: %s\n", reply->str);
}
freeReplyObject(reply);
// 随机弹出多个元素
reply = redisCommand(c, "SPOP myset 3");
if (reply->type == REDIS_REPLY_ARRAY) {
printf("随机弹出的3个元素: ");
for (size_t i = 0; i < reply->elements; i++) {
printf("%s ", reply->element[i]->str);
}
printf("\n");
}
freeReplyObject(reply);
注意:SPOP会移除被弹出的元素。如果你只是想随机查看但不删除,应该用SRANDMEMBER。我刚开始用的时候没注意这个区别,结果抽奖时把奖品池里的元素都弹没了,后面的人就抽不到了。
6. 知识体系总览
下面这张图把Redis集合操作的核心命令和它们之间的关系梳理清楚了。建议你保存下来,写代码时对照着看。
7. 实战:用户共同关注功能
最后来个完整的例子。假设我们要实现一个「用户共同关注」功能,用集合操作轻松搞定。
#include <stdio.h>
#include <hiredis/hiredis.h>
void print_set(redisContext *c, const char *key) {
redisReply *reply = redisCommand(c, "SMEMBERS %s", key);
printf("%s: ", key);
for (size_t i = 0; i < reply->elements; i++) {
printf("%s ", reply->element[i]->str);
}
printf("\n");
freeReplyObject(reply);
}
int main() {
redisContext *c = redisConnect("127.0.0.1", 6379);
if (c == NULL || c->err) {
printf("连接失败\n");
return 1;
}
// 用户A关注了:Redis, C, Linux, Docker
redisCommand(c, "SADD user:1001:follow Redis C Linux Docker");
// 用户B关注了:C, Python, Linux, Kubernetes
redisCommand(c, "SADD user:1002:follow C Python Linux Kubernetes");
// 查看各自关注列表
print_set(c, "user:1001:follow");
print_set(c, "user:1002:follow");
// 计算共同关注
redisReply *reply = redisCommand(c, "SINTER user:1001:follow user:1002:follow");
printf("共同关注: ");
for (size_t i = 0; i < reply->elements; i++) {
printf("%s ", reply->element[i]->str);
}
printf("\n");
freeReplyObject(reply);
// 计算A关注但B没关注的
reply = redisCommand(c, "SDIFF user:1001:follow user:1002:follow");
printf("A独有关注: ");
for (size_t i = 0; i < reply->elements; i++) {
printf("%s ", reply->element[i]->str);
}
printf("\n");
freeReplyObject(reply);
// 计算总关注数
reply = redisCommand(c, "SUNION user:1001:follow user:1002:follow");
printf("总关注数: %zu\n", reply->elements);
freeReplyObject(reply);
redisFree(c);
return 0;
}
你看,几行代码就把共同关注、独有关注、总关注数全算出来了。这就是集合的魅力——用最少的代码,解决最复杂的问题。
总结一下:Redis集合操作的核心就这些命令。SADD/SREM管增删,SMEMBERS/SCARD管查看,SINTER/SUNION/SDIFF管运算,SPOP管随机。掌握了这些,大部分集合场景你都能应付。剩下的就是多写多练,把命令变成肌肉记忆。
公众号:蓝海资料掘金营,微信deep3321