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集合操作的核心命令和它们之间的关系梳理清楚了。建议你保存下来,写代码时对照着看。

Redis集合操作知识体系 Redis Set SADD / SREM SMEMBERS / SCARD SPOP / SRANDMEMBER SINTER(交集) SUNION(并集) SDIFF(差集) 💡 使用建议: • 小集合用SMEMBERS,大集合用SSCAN游标遍历 • SPOP会删除元素,SRANDMEMBER只查看不删除 • 集合运算结果集较大时,建议用SINTERSTORE等存储命令

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