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) 单个操作 |
最佳实践:
- 使用 Set 去重
- 大集合使用 SSCAN
- 集合运算注意性能
- 定期清理过期数据
掌握 Set,高效处理去重和集合运算!