文章目录
- 1. set的基本介绍
- 1.1. set底层结构之hash表的简单介绍
- 1.2. 常用命令
- 2. 常见的业务场景
- 2.1. 标签系统
- 2.2. 社交网络好友关系
1. set的基本介绍
参考链接:https://mp.weixin.qq.com/s/srkd73bS2n3mjIADLVg72A
redis 的 set 数据结构是一个无序的集合,可以存储不重复的元素。适合于多种应用场景,尤其是在需要去重、快速查找和集合运算的场合。从上面的介绍可以看到set主要的特性为:
无序性:元素在set中没有特定的顺序;
唯一性:set中的元素是唯一的,不能重复。
其中还有一个比较重要的特性就是高效性。因为redis 的 Set 数据结构是基于哈希表(hash table)实现的,这也是为什么对元素的添加、删除和查找操作的时间复杂度都是 O(1) 的原因。
1.1. set底层结构之hash表的简单介绍
- 哈希函数:哈希表使用哈希函数将元素映射到一个数组的索引中。通过这个索引,我们可以快速定位到存储在该位置的元素。
- 存储结构:在 Redis 中,Set 存储元素时会使用一个哈希表,其键是元素的值,值通常是一个指向该元素的指针。这样,每个元素的插入、查找或删除操作都可以直接通过哈希表的索引进行。
- 添加元素(SADD):当添加一个新元素时,Redis 计算该元素的哈希值,并根据这个哈希值找到相应的索引位置。如果该位置为空(即没有相同的元素),就将该元素插入数组中。这一过程的时间复杂度是 O(1)。
- 删除元素(SREM):删除一个元素时,Redis 同样计算该元素的哈希值并找到相应的索引位置。如果该元素存在,就将其从哈希表中移除。这一过程也是 O(1)。
- 查找元素(SISMEMBER):查找一个元素是否在集合中,Redis 计算该元素的哈希值,并通过该哈希值定位到对应的索引位置。如果该元素存在于该位置,则返回 true;如果不存在,则返回 false。这一过程同样是 O(1)。
理论上的限制
尽管哈希表的平均时间复杂度是 O(1),在某些情况下,哈希表可能会出现冲突(即不同的元素计算出相同的哈希值)。当发生冲突时,多个元素可能会存储在同一个索引位置,导致查找、插入和删除的性能下降。此时,复杂度可能退化为 O(n),其中 n 是发生冲突的元素数量。然而,Redis 使用了一些策略(如动态扩容和链表/树结构来处理冲突)来保持哈希表的性能,使得在大多数情况下,操作的时间复杂度仍然可以保持在 O(1)。
1.2. 常用命令
SADD key member [member ...]:将一个或多个成员添加到集合中。
SREM key member [member ...]:移除集合中的一个或多个成员。
SISMEMBER key member:判断某个成员是否是集合中的成员。
SMEMBERS key:返回集合中的所有成员。
SCARD key:返回集合中成员的数量。
交集:SINTER key1 [key2 ...]:返回所有给定集合的交集。
并集:SUNION key1 [key2 ...]:返回所有给定集合的并集。
差集:SDIFF key1 [key2 ...]:返回第一个集合与其他集合的差集。
SADD myset "apple" "banana" "orange"
SREM myset "banana"
SISMEMBER myset "apple" # 返回 1 (true)
SMEMBERS myset # 返回 ["apple", "orange"]
SCARD myset # 返回 2
SADD set1 "a" "b" "c"
SADD set2 "b" "c" "d"
SINTER set1 set2 # 返回 ["b", "c"]
SUNION set1 set2 # 返回 ["a", "b", "c", "d"]
SDIFF set1 set2 # 返回 ["a"]
2. 常见的业务场景
一般业务场景的使用是会根据对应数据结构的特性来说明的:
- 去重:当需要存储一组唯一的用户 ID、标签或其他信息时,可以使用 Set 来自动去重。例如,存储用户的浏览历史,确保每个用户的历史记录中没有重复的页面。【唯一性】
- 社交网络:在社交网络应用中,可以使用 Set 来表示用户的好友关系。比如,用户 A 的好友可以存储在 Set 中,快速判断用户 B 是否是用户 A 的好友。【唯一性,高效性】
- 标签系统:对于文章、商品等,可以使用 Set 来存储相关标签,方便进行标签查询和管理。【高效性】
- 投票系统:可以使用 Set 存储已经投票的用户 ID,确保每个用户只能投一次票。【唯一性】
- 实时分析:在实时分析场景中,可以使用 Set 来跟踪活跃用户、访问过的页面等。【高效性】
2.1. 标签系统
标签系统:Set类型可用于存储和处理具有标签特性的数据,如商品标签、文章分类标签等。
背景
在一个内容平台上,用户可以给文章打上不同的标签,系统需要根据标签过滤和推荐文章。
优势
- 快速查找:使用Set可以快速判断一个元素是否属于某个集合。
- 灵活的标签管理:方便地添加和删除标签,实现标签的灵活管理。
- 集合运算:通过集合运算,如交集和并集,可以轻松实现复杂的标签过滤逻辑。
代码实现
package main
import (
"context"
"fmt"
"github.com/go-redis/redis/v8"
)
var ctx = context.Background()
// Redis 客户端初始化
var rdb = redis.NewClient(&redis.Options{
Addr: "", // Redis 服务器地址
Password: "", // 密码
DB: 0, // 使用默认 DB
})
// 文章标签管理结构体
type ArticleTagManager struct{}
// 给文章添加标签
func (atm *ArticleTagManager) AddTagToArticle(articleID string, tags []string) error {
_, err := rdb.SAdd(ctx, "article:"+articleID+":tags", tags).Result()
if err != nil {
return err
}
for _, tag := range tags {
_, err := rdb.SAdd(ctx, "global:tags:"+tag, "article:"+articleID).Result()
if err != nil {
return err
}
}
log.Printf("Added tags %v to article %s\n", tags, articleID)
return nil
}
// 从文章中删除标签
func (atm *ArticleTagManager) RemoveTagFromArticle(articleID string, tag string) error {
_, err := rdb.SRem(ctx, "article:"+articleID+":tags", tag).Result()
if err != nil {
return err
}
log.Printf("Removed tag %s from article %s\n", tag, articleID)
return nil
}
// 获取文章的所有标签
func (atm *ArticleTagManager) GetTagsOfArticle(articleID string) ([]string, error) {
tags, err := rdb.SMembers(ctx, "article:"+articleID+":tags").Result()
if err != nil {
return nil, err
}
return tags, nil
}
// 根据标签获取文章列表
func (atm *ArticleTagManager) GetArticlesByTag(tag string) ([]string, error) {
articles, err := rdb.SMembers(ctx, "global:tags:"+tag).Result()
if err != nil {
return nil, err
}
return articles, nil
}
// 获取文章的标签数量
func (atm *ArticleTagManager) GetTagCountOfArticle(articleID string) (int64, error) {
count, err := rdb.SCard(ctx, "article:"+articleID+":tags").Result()
if err != nil {
return 0, err
}
return count, nil
}
// 获取某个标签的文章数量
func (atm *ArticleTagManager) GetArticleCountByTag(tag string) (int64, error) {
count, err := rdb.SCard(ctx, "global:tags:"+tag).Result()
if err != nil {
return 0, err
}
return count, nil
}
func main() {
atm := &ArticleTagManager{}
// 为文章添加标签
if err := atm.AddTagToArticle("1", []string{"Golang", "Redis"}); err != nil {
log.Fatalf("Error adding tags: %v", err)
}
if err := atm.AddTagToArticle("2", []string{"Python", "Redis"}); err != nil {
log.Fatalf("Error adding tags: %v", err)
}
if err := atm.AddTagToArticle("3", []string{"Golang", "Python"}); err != nil {
log.Fatalf("Error adding tags: %v", err)
}
// 获取文章的所有标签
if tags, err := atm.GetTagsOfArticle("1"); err == nil {
fmt.Println("Tags for article 1:")
for _, tag := range tags {
fmt.Println(tag)
}
}
// 根据标签获取文章列表
if articles, err := atm.GetArticlesByTag("Golang"); err == nil {
fmt.Println("Articles with tag 'Golang':")
for _, article := range articles {
fmt.Println(article)
}
}
// 获取文章标签数量
if count, err := atm.GetTagCountOfArticle("1"); err == nil {
fmt.Printf("Article 1 has %d tags.\n", count)
}
// 获取某个标签的文章数量
if count, err := atm.GetArticleCountByTag("Redis"); err == nil {
fmt.Printf("Tag 'Redis' has %d articles associated.\n", count)
}
// 测试完移除整个集合
rdb.Del(ctx, "article:1:tags")
rdb.Del(ctx, "article:2:tags")
rdb.Del(ctx, "article:3:tags")
rdb.Del(ctx, "global:tags:Golang")
rdb.Del(ctx, "global:tags:Redis")
rdb.Del(ctx, "global:tags:Python")
}
运行结果:
2.2. 社交网络好友关系
社交网络好友关系:Set类型可以表示用户的好友列表,支持快速好友关系测试和好友推荐。
背景
在一个社交网络应用中,用户可以添加和删除好友,系统需要管理用户的好友关系。
优势
- 唯一性:保证好友列表中不会有重复的好友。
- 快速关系测试:快速判断两个用户是否互为好友。
- 好友推荐:利用集合运算,如差集,推荐可能认识的好友。
解决方案
使用Redis Set类型存储用户的好友集合,实现好友关系的管理。
代码实现
package main
import (
"context"
"fmt"
"github.com/go-redis/redis/v8"
"log"
)
var ctx = context.Background()
// Redis 客户端初始化
var rdb = redis.NewClient(&redis.Options{
Addr: "", // Redis 服务器地址
Password: "", // 密码
DB: 0, // 使用默认 DB
})
// 添加好友
func addFriend(userOneID string, userTwoID string) error {
_, err := rdb.SAdd(ctx, "user:"+userOneID+":friends", userTwoID).Result()
if err != nil {
return err
}
_, err = rdb.SAdd(ctx, "user:"+userTwoID+":friends", userOneID).Result()
return err
}
// 判断是否是好友
func isFriend(userOneID string, userTwoID string) bool {
return rdb.SIsMember(ctx, "user:"+userOneID+":friends", userTwoID).Val()
}
// 获取用户的好友列表
func getFriendsOfUser(userID string) ([]string, error) {
friends, err := rdb.SMembers(ctx, "user:"+userID+":friends").Result()
return friends, err
}
// 删除好友
func removeFriend(userOneID string, userTwoID string) error {
_, err := rdb.SRem(ctx, "user:"+userOneID+":friends", userTwoID).Result()
if err != nil {
return err
}
_, err = rdb.SRem(ctx, "user:"+userTwoID+":friends", userOneID).Result()
return err
}
// 推荐可能认识的好友
func recommendFriends(userID string) ([]string, error) {
// 获取用户的好友
friends, err := getFriendsOfUser(userID)
if err != nil {
return nil, err
}
// 如果没有好友,则没有推荐
if len(friends) == 0 {
return nil, nil
}
// 找到所有好友的好友,以及去掉自己的好友和自己
potentialFriends := make(map[string]struct{})
for _, friendID := range friends {
friendFriends, err := getFriendsOfUser(friendID)
if err != nil {
return nil, err
}
for _, potentialFriend := range friendFriends {
// 排除自己和直接好友
if potentialFriend != userID {
potentialFriends[potentialFriend] = struct{}{}
}
}
}
// 将推荐的好友转换为切片
recommendations := []string{}
for friend := range potentialFriends {
// 确保不是已经存在的好友
if !isFriend(userID, friend) {
recommendations = append(recommendations, friend)
}
}
return recommendations, nil
}
func main() {
// 示例:添加好友
if err := addFriend("user1", "user2"); err != nil {
log.Fatalf("Error adding friend: %v", err)
}
// 示例:检查好友关系
if isFriend("user1", "user2") {
fmt.Println("user1 and user2 are friends.")
}
// 示例:获取好友列表
friends, err := getFriendsOfUser("user1")
if err != nil {
log.Fatalf("Error getting friends: %v", err)
}
fmt.Println("Friends of user1:", friends)
// 示例:推荐可能认识的好友
recommendations, err := recommendFriends("user1")
if err != nil {
log.Fatalf("Error recommending friends: %v", err)
}
fmt.Println("Recommended friends for user1:", recommendations)
// 示例:删除好友
if err := removeFriend("user1", "user2"); err != nil {
log.Fatalf("Error removing friend: %v", err)
}
}
注意事项:
- 虽然Set是无序的,但Redis会保持元素的插入顺序,直到集合被重新排序。
- Set中的元素是唯一的,任何尝试添加重复元素的操作都会无效。
- 使用集合运算时,需要注意结果集的大小,因为它可能会影响性能。