Skip to content
清晨的一缕阳光
返回

Redis Set 数据类型详解

Redis Set 数据类型详解

Set 是 Redis 的无序集合,元素唯一且支持集合运算。本文将深入 Set 的底层结构,掌握去重、集合运算等核心应用场景。

一、Set 基础概念

1.1 什么是 Set

Set 是无序不重复集合

# 添加元素
SADD tags "java" "golang" "redis" "java"  # java 重复,只存一次

# 获取所有元素
SMEMBERS tags
# 1) "java"
# 2) "golang"
# 3) "redis"

# 检查元素是否存在
SISMEMBER tags "java"    # 1 (存在)
SISMEMBER tags "python"  # 0 (不存在)

# 集合大小
SCARD tags  # 3

二、底层数据结构

2.1 整数集合(intset)

小整数集合使用

// intset 结构
typedef struct intset {
    uint32_t encoding;  // 编码方式
    uint32_t length;    // 元素数量
    int8_t contents[];  // 元素数组
} intset;

触发条件

# 全部是整数且数量 < 512 时使用 intset
set-max-intset-entries 512

2.2 字典(dict)

大集合或非整数使用

// 与 Hash 相同的字典结构
typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    // ...
} dict;

三、核心命令详解

3.1 基本操作

# 添加元素
SADD key member [member ...]

# 删除元素
SREM key member [member ...]

# 随机弹出
SPOP key [count]

# 随机获取(不删除)
SRANDMEMBER key [count]

# 获取所有元素
SMEMBERS key

# 检查元素
SISMEMBER key member

3.2 集合运算

# 差集
SDIFF key1 key2

# 差集并存储
SDIFFSTORE dest key1 key2

# 交集
SINTER key1 key2

# 交集并存储
SINTERSTORE dest key1 key2

# 并集
SUNION key1 key2

# 并集并存储
SUNIONSTORE dest key1 key2

3.3 迭代操作

# 迭代集合
SSCAN key cursor [MATCH pattern] [COUNT count]

# 示例
SSCAN tags 0 MATCH "j*" COUNT 10

四、Java 实现示例

4.1 基础操作

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import java.util.Set;

public class RedisSetExample {
    private static JedisPool pool = new JedisPool("localhost", 6379);
    
    public static void main(String[] args) {
        try (Jedis jedis = pool.getResource()) {
            String key = "tags";
            
            // 添加元素
            jedis.sadd(key, "java", "golang", "redis", "java");
            
            // 获取所有元素
            Set<String> tags = jedis.smembers(key);
            System.out.println("Tags: " + tags);
            
            // 检查元素
            boolean exists = jedis.sismember(key, "java");
            System.out.println("java exists: " + exists);
            
            // 集合大小
            long size = jedis.scard(key);
            System.out.println("Size: " + size);
            
            // 删除元素
            jedis.srem(key, "redis");
            
            // 随机弹出一个元素
            String randomTag = jedis.spop(key);
            System.out.println("Random tag: " + randomTag);
        }
    }
}

4.2 集合运算

import redis.clients.jedis.Jedis;
import java.util.Set;

public class RedisSetOperations {
    private static Jedis jedis = new Jedis("localhost", 6379);
    
    public static void main(String[] args) {
        // 用户 A 的标签
        jedis.sadd("user:A:tags", "java", "golang", "redis");
        
        // 用户 B 的标签
        jedis.sadd("user:B:tags", "java", "python", "mysql");
        
        // 共同标签(交集)
        Set<String> common = jedis.sinter("user:A:tags", "user:B:tags");
        System.out.println("Common tags: " + common);  // [java]
        
        // A 有 B 没有的标签(差集)
        Set<String> diff = jedis.sdiff("user:A:tags", "user:B:tags");
        System.out.println("A unique tags: " + diff);  // [golang, redis]
        
        // 所有标签(并集)
        Set<String> all = jedis.sunion("user:A:tags", "user:B:tags");
        System.out.println("All tags: " + all);
    }
}

4.3 去重计数器

import redis.clients.jedis.Jedis;

public class UniqueCounter {
    private static Jedis jedis = new Jedis("localhost", 6379);
    
    /**
     * 统计独立访客(UV)
     */
    public static void recordVisit(String pageId, String userId) {
        String key = "page:" + pageId + ":visitors";
        jedis.sadd(key, userId);
    }
    
    /**
     * 获取 UV 数量
     */
    public static long getUV(String pageId) {
        String key = "page:" + pageId + ":visitors";
        return jedis.scard(key);
    }
    
    /**
     * 检查用户是否已访问
     */
    public static boolean hasVisited(String pageId, String userId) {
        String key = "page:" + pageId + ":visitors";
        return jedis.sismember(key, userId);
    }
    
    public static void main(String[] args) {
        // 记录访问
        recordVisit("article:1001", "user:1");
        recordVisit("article:1001", "user:2");
        recordVisit("article:1001", "user:1");  // 重复访问
        
        // 获取 UV
        long uv = getUV("article:1001");
        System.out.println("UV: " + uv);  // 2
        
        // 检查是否访问过
        boolean visited = hasVisited("article:1001", "user:1");
        System.out.println("User 1 visited: " + visited);  // true
    }
}

五、Golang 实现示例

5.1 基础操作

package main

import (
    "fmt"
    "github.com/go-redis/redis/v8"
    "context"
)

func main() {
    ctx := context.Background()
    rdb := redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
    })
    
    key := "tags"
    
    // 添加元素
    rdb.SAdd(ctx, key, "java", "golang", "redis", "java")
    
    // 获取所有元素
    tags, _ := rdb.SMembers(ctx, key).Result()
    fmt.Println("Tags:", tags)
    
    // 检查元素
    exists, _ := rdb.SIsMember(ctx, key, "java").Result()
    fmt.Println("java exists:", exists)
    
    // 集合大小
    size, _ := rdb.SCard(ctx, key).Result()
    fmt.Println("Size:", size)
    
    // 删除元素
    rdb.SRem(ctx, key, "redis")
    
    // 随机弹出一个元素
    tag, _ := rdb.SPop(ctx, key).Result()
    fmt.Println("Random tag:", tag)
}

5.2 集合运算

package main

import (
    "fmt"
    "github.com/go-redis/redis/v8"
    "context"
)

func main() {
    ctx := context.Background()
    rdb := redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
    })
    
    // 用户 A 的标签
    rdb.SAdd(ctx, "user:A:tags", "java", "golang", "redis")
    
    // 用户 B 的标签
    rdb.SAdd(ctx, "user:B:tags", "java", "python", "mysql")
    
    // 共同标签(交集)
    common, _ := rdb.SInter(ctx, "user:A:tags", "user:B:tags").Result()
    fmt.Println("Common tags:", common)  // [java]
    
    // A 有 B 没有的标签(差集)
    diff, _ := rdb.SDiff(ctx, "user:A:tags", "user:B:tags").Result()
    fmt.Println("A unique tags:", diff)  // [golang, redis]
    
    // 所有标签(并集)
    all, _ := rdb.SUnion(ctx, "user:A:tags", "user:B:tags").Result()
    fmt.Println("All tags:", all)
}

5.3 好友关系

package main

import (
    "fmt"
    "github.com/go-redis/redis/v8"
    "context"
)

type FriendManager struct {
    rdb *redis.Client
}

func NewFriendManager(rdb *redis.Client) *FriendManager {
    return &FriendManager{rdb: rdb}
}

// 添加好友
func (fm *FriendManager) AddFriend(userID, friendID string) {
    ctx := context.Background()
    fm.rdb.SAdd(ctx, fm.friendKey(userID), friendID)
    fm.rdb.SAdd(ctx, fm.friendKey(friendID), userID)
}

// 删除好友
func (fm *FriendManager) RemoveFriend(userID, friendID string) {
    ctx := context.Background()
    fm.rdb.SRem(ctx, fm.friendKey(userID), friendID)
    fm.rdb.SRem(ctx, fm.friendKey(friendID), userID)
}

// 获取好友列表
func (fm *FriendManager) GetFriends(userID string) []string {
    ctx := context.Background()
    friends, _ := fm.rdb.SMembers(ctx, fm.friendKey(userID)).Result()
    return friends
}

// 检查是否是好友
func (fm *FriendManager) IsFriend(userID, friendID string) bool {
    ctx := context.Background()
    isFriend, _ := fm.rdb.SIsMember(ctx, fm.friendKey(userID), friendID).Result()
    return isFriend
}

// 获取共同好友
func (fm *FriendManager) GetMutualFriends(userID1, userID2 string) []string {
    ctx := context.Background()
    friends, _ := fm.rdb.SInter(ctx, fm.friendKey(userID1), fm.friendKey(userID2)).Result()
    return friends
}

func (fm *FriendManager) friendKey(userID string) string {
    return "user:" + userID + ":friends"
}

func main() {
    ctx := context.Background()
    rdb := redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
    })
    
    fm := NewFriendManager(rdb)
    
    // 添加好友
    fm.AddFriend("user:1", "user:2")
    fm.AddFriend("user:1", "user:3")
    fm.AddFriend("user:2", "user:3")
    
    // 获取好友列表
    friends := fm.GetFriends("user:1")
    fmt.Println("User 1 friends:", friends)
    
    // 检查好友关系
    isFriend := fm.IsFriend("user:1", "user:2")
    fmt.Println("User 1 and 2 are friends:", isFriend)
    
    // 获取共同好友
    mutual := fm.GetMutualFriends("user:1", "user:2")
    fmt.Println("Mutual friends:", mutual)
}

六、应用场景

6.1 标签系统

public class TagManager {
    private Jedis jedis;
    
    public TagManager(Jedis jedis) {
        this.jedis = jedis;
    }
    
    // 为文章添加标签
    public void addTags(String articleId, String... tags) {
        String key = "article:" + articleId + ":tags";
        jedis.sadd(key, tags);
    }
    
    // 获取文章标签
    public Set<String> getTags(String articleId) {
        String key = "article:" + articleId + ":tags";
        return jedis.smembers(key);
    }
    
    // 根据标签搜索文章
    public Set<String> searchByTag(String tag) {
        String key = "tag:" + tag + ":articles";
        return jedis.smembers(key);
    }
    
    // 获取相关文章(共同标签)
    public Set<String> getRelatedArticles(String articleId1, String articleId2) {
        String key1 = "article:" + articleId1 + ":tags";
        String key2 = "article:" + articleId2 + ":tags";
        return jedis.sinter(key1, key2);
    }
}

6.2 抽奖系统

public class LotterySystem {
    private Jedis jedis;
    
    public LotterySystem(Jedis jedis) {
        this.jedis = jedis;
    }
    
    // 添加参与者
    public void addParticipant(String lotteryId, String userId) {
        String key = "lottery:" + lotteryId + ":participants";
        jedis.sadd(key, userId);
    }
    
    // 随机抽取获奖者
    public String drawWinner(String lotteryId) {
        String key = "lottery:" + lotteryId + ":participants";
        return jedis.spop(key);  // 弹出并删除
    }
    
    // 抽取多个获奖者
    public Set<String> drawWinners(String lotteryId, int count) {
        String key = "lottery:" + lotteryId + ":participants";
        return jedis.spop(key, count);
    }
    
    // 查看剩余参与者
    public long getRemainingParticipants(String lotteryId) {
        String key = "lottery:" + lotteryId + ":participants";
        return jedis.scard(key);
    }
}

6.3 黑名单管理

type BlacklistManager struct {
    rdb *redis.Client
}

func NewBlacklistManager(rdb *redis.Client) *BlacklistManager {
    return &BlacklistManager{rdb: rdb}
}

// 添加到黑名单
func (bm *BlacklistManager) AddToBlacklist(userID string) {
    ctx := context.Background()
    bm.rdb.SAdd(ctx, "blacklist:users", userID)
}

// 从黑名单移除
func (bm *BlacklistManager) RemoveFromBlacklist(userID string) {
    ctx := context.Background()
    bm.rdb.SRem(ctx, "blacklist:users", userID)
}

// 检查是否在黑名单
func (bm *BlacklistManager) IsBlacklisted(userID string) bool {
    ctx := context.Background()
    isBlocked, _ := bm.rdb.SIsMember(ctx, "blacklist:users", userID).Result()
    return isBlocked
}

// 获取所有黑名单用户
func (bm *BlacklistManager) GetAllBlacklisted() []string {
    ctx := context.Background()
    users, _ := bm.rdb.SMembers(ctx, "blacklist:users").Result()
    return users
}

func main() {
    rdb := redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
    })
    
    bm := NewBlacklistManager(rdb)
    
    // 添加黑名单
    bm.AddToBlacklist("user:bad")
    
    // 检查
    if bm.IsBlacklisted("user:bad") {
        fmt.Println("User is blacklisted")
    }
}

七、性能优化

7.1 批量操作

// 使用 Pipeline 批量添加
public void batchAddTags(String articleId, List<String> tags) {
    String key = "article:" + articleId + ":tags";
    
    Pipeline pipe = jedis.pipelined();
    for (String tag : tags) {
        pipe.sadd(key, tag);
    }
    pipe.sync();
}

7.2 迭代大集合

// 使用 SSCAN 迭代大集合
func scanLargeSet(rdb *redis.Client, key string) []string {
    ctx := context.Background()
    var result []string
    
    cursor := uint64(0)
    for {
        keys, nextCursor, _ := rdb.SScan(ctx, key, cursor, "", 100).Result()
        result = append(result, keys...)
        
        if nextCursor == 0 {
            break
        }
        cursor = nextCursor
    }
    
    return result
}

7.3 限制集合大小

// 限制标签数量
public void addTagWithLimit(String articleId, String tag, int maxTags) {
    String key = "article:" + articleId + ":tags";
    
    Pipeline pipe = jedis.pipelined();
    pipe.sadd(key, tag);
    
    // 如果超过限制,随机删除
    Long size = pipe.scard(key).get(0);
    if (size > maxTags) {
        pipe.spop(key, 1);
    }
    
    pipe.sync();
}

八、最佳实践

8.1 使用建议

场景推荐方案
去重Set
海量去重HyperLogLog
集合运算Set
有序列表ZSet

8.2 注意事项

1. SMEMBERS 可能阻塞(大集合)
   → 使用 SSCAN 迭代

2. 集合运算消耗大
   → 限制参与运算的集合大小

3. 内存占用
   → 定期清理不用的集合

总结

Redis Set 核心要点:

特性说明
底层结构intset(小)/ dict(大)
唯一性自动去重
集合运算交/并/差集
时间复杂度O(1) 单个操作

最佳实践

  1. 使用 Set 去重
  2. 大集合使用 SSCAN
  3. 集合运算注意性能
  4. 定期清理过期数据

掌握 Set,高效处理去重和集合运算!

参考资料


分享这篇文章到:

上一篇文章
RAG 文档处理工程化实战
下一篇文章
MySQL 性能监控与诊断