竹简文档
缓存

SetCache

基于集合数据结构的无序唯一元素缓存接口

SetCache

SetCache 定义了基于集合(Set)数据结构的缓存操作接口,用于管理无序且元素唯一的集合数据。

接口定义

type SetCache[K any, V any] interface {
    Add(ctx context.Context, key K, members ...V) error
    Members(ctx context.Context, key K) ([]V, error)
    IsMember(ctx context.Context, key K, member V) (bool, error)
    Count(ctx context.Context, key K) (int64, error)
    Remove(ctx context.Context, key K, members ...V) error
    Delete(ctx context.Context, key K) error
}

泛型参数

字段

类型

数据特点

  • 无序性:成员没有固定顺序
  • 唯一性:相同成员只会存储一次
  • 高效性:成员检查时间复杂度 O(1)
user:tags:123 → {"golang", "redis", "docker"}

方法说明

Add

将一个或多个成员添加到集合中。

Add(ctx context.Context, key K, members ...V) error

参数:

  • ctx - context.Context 上下文
  • key - 集合键
  • members - 要添加的成员(可变参数)

返回值:

  • error - 错误信息

注意: 已存在的成员会被忽略。

Members

获取集合中的所有成员。

Members(ctx context.Context, key K) ([]V, error)

参数:

  • ctx - context.Context 上下文
  • key - 集合键

返回值:

  • []V - 所有成员的切片
  • error - 错误信息

IsMember

检查指定成员是否存在于集合中。

IsMember(ctx context.Context, key K, member V) (bool, error)

参数:

  • ctx - context.Context 上下文
  • key - 集合键
  • member - 要检查的成员

返回值:

  • bool - 成员是否存在
  • error - 错误信息

Count

获取集合中的成员数量。

Count(ctx context.Context, key K) (int64, error)

参数:

  • ctx - context.Context 上下文
  • key - 集合键

返回值:

  • int64 - 成员数量
  • error - 错误信息

Remove

从集合中移除指定的成员。

Remove(ctx context.Context, key K, members ...V) error

参数:

  • ctx - context.Context 上下文
  • key - 集合键
  • members - 要移除的成员(可变参数)

返回值:

  • error - 错误信息

Delete

删除整个集合。

Delete(ctx context.Context, key K) error

参数:

  • ctx - context.Context 上下文
  • key - 集合键

返回值:

  • error - 错误信息

实现示例

用户标签缓存

cache/user_tags.go
import (
    "github.com/gin-gonic/gin"
    xCache "github.com/bamboo-services/bamboo-base-go/major/cache"
)

type UserTagsCache struct {
    *xCache.Cache
}

// Add 添加用户标签
func (c *UserTagsCache) Add(ctx context.Context, userID string, tags ...string) error {
    key := "user:tags:" + userID
    return c.RDB.SAdd(ctx, key, tags).Err()
}

// Members 获取所有标签
func (c *UserTagsCache) Members(ctx context.Context, userID string) ([]string, error) {
    key := "user:tags:" + userID
    return c.RDB.SMembers(ctx, key).Result()
}

// IsMember 检查标签是否存在
func (c *UserTagsCache) IsMember(ctx context.Context, userID string, tag string) (bool, error) {
    key := "user:tags:" + userID
    return c.RDB.SIsMember(ctx, key, tag).Result()
}

// Count 获取标签数量
func (c *UserTagsCache) Count(ctx context.Context, userID string) (int64, error) {
    key := "user:tags:" + userID
    return c.RDB.SCard(ctx, key).Result()
}

// Remove 移除标签
func (c *UserTagsCache) Remove(ctx context.Context, userID string, tags ...string) error {
    key := "user:tags:" + userID
    return c.RDB.SRem(ctx, key, tags).Err()
}

// Delete 删除所有标签
func (c *UserTagsCache) Delete(ctx context.Context, userID string) error {
    key := "user:tags:" + userID
    return c.RDB.Del(ctx, key).Err()
}

使用标签缓存

service/user_tags.go
type UserTagsService struct {
    cache *UserTagsCache
}

func NewUserTagsService(cache *xCache.Cache) *UserTagsService {
    return &UserTagsService{
        cache: &UserTagsCache{Cache: cache},
    }
}

// AddTags 添加用户标签
func (s *UserTagsService) AddTags(ctx context.Context, userID string, tags ...string) error {
    // 更新数据库
    if err := s.addTagsToDB(userID, tags...); err != nil {
        return err
    }

    // 更新缓存
    return s.cache.Add(ctx, userID, tags...)
}

// GetTags 获取用户标签
func (s *UserTagsService) GetTags(ctx context.Context, userID string) ([]string, error) {
    // 先从缓存获取
    tags, err := s.cache.Members(ctx, userID)
    if err != nil {
        return nil, err
    }

    // 如果缓存为空,从数据库加载
    if len(tags) == 0 {
        tags, err = s.getTagsFromDB(userID)
        if err != nil {
            return nil, err
        }

        // 写入缓存
        if len(tags) > 0 {
            _ = s.cache.Add(ctx, userID, tags...)
        }
    }

    return tags, nil
}

// HasTag 检查用户是否有指定标签
func (s *UserTagsService) HasTag(ctx context.Context, userID string, tag string) (bool, error) {
    return s.cache.IsMember(ctx, userID, tag)
}

// RemoveTags 移除用户标签
func (s *UserTagsService) RemoveTags(ctx context.Context, userID string, tags ...string) error {
    // 更新数据库
    if err := s.removeTagsFromDB(userID, tags...); err != nil {
        return err
    }

    // 更新缓存
    return s.cache.Remove(ctx, userID, tags...)
}

使用场景

用户标签

type UserTagsCache interface {
    xCache.SetCache[string, string]
}

适用于:

  • 用户兴趣标签
  • 技能标签
  • 分类标签

权限管理

type PermissionCache interface {
    xCache.SetCache[string, string]
}

适用于:

  • 用户权限列表
  • 角色权限
  • 资源访问权限

在线用户

type OnlineUsersCache interface {
    xCache.SetCache[string, string]
}

适用于:

  • 在线用户集合
  • 活跃用户统计
  • 房间成员列表

去重场景

type UniqueItemsCache interface {
    xCache.SetCache[string, string]
}

适用于:

  • 已处理的任务 ID
  • 去重的消息 ID
  • 唯一的访客 IP

最佳实践

1. 批量添加

使用可变参数一次添加多个成员:

// ✅ 批量添加
cache.Add(ctx, userID, "golang", "redis", "docker")

// ❌ 逐个添加
cache.Add(ctx, userID, "golang")
cache.Add(ctx, userID, "redis")
cache.Add(ctx, userID, "docker")

2. 成员检查

使用 IsMember 而不是 Members

// ✅ 高效检查
exists, _ := cache.IsMember(ctx, userID, "golang")

// ❌ 低效检查
members, _ := cache.Members(ctx, userID)
for _, m := range members {
    if m == "golang" {
        exists = true
        break
    }
}

3. 数量统计

使用 Count 而不是 Members

// ✅ 高效统计
count, _ := cache.Count(ctx, userID)

// ❌ 低效统计
members, _ := cache.Members(ctx, userID)
count := len(members)

4. 空集合处理

区分空集合和不存在的集合:

func (s *UserTagsService) GetTags(ctx context.Context, userID string) ([]string, error) {
    tags, err := s.cache.Members(ctx, userID)
    if err != nil {
        return nil, err
    }

    // 空切片表示集合存在但为空
    // nil 表示集合不存在
    if tags == nil {
        tags = []string{}
    }

    return tags, nil
}

高级操作

集合运算

虽然接口不直接支持,但可以使用 Redis 命令:

// 交集:获取两个用户的共同标签
func (c *UserTagsCache) CommonTags(ctx context.Context, userID1, userID2 string) ([]string, error) {
    key1 := "user:tags:" + userID1
    key2 := "user:tags:" + userID2
    return c.RDB.SInter(ctx, key1, key2).Result()
}

// 并集:获取两个用户的所有标签
func (c *UserTagsCache) AllTags(ctx context.Context, userID1, userID2 string) ([]string, error) {
    key1 := "user:tags:" + userID1
    key2 := "user:tags:" + userID2
    return c.RDB.SUnion(ctx, key1, key2).Result()
}

// 差集:获取用户1独有的标签
func (c *UserTagsCache) UniqueTags(ctx context.Context, userID1, userID2 string) ([]string, error) {
    key1 := "user:tags:" + userID1
    key2 := "user:tags:" + userID2
    return c.RDB.SDiff(ctx, key1, key2).Result()
}

随机获取

随机获取成员(用于推荐等场景):

// 随机获取一个标签
func (c *UserTagsCache) RandomTag(ctx context.Context, userID string) (string, error) {
    key := "user:tags:" + userID
    return c.RDB.SRandMember(ctx, key).Result()
}

// 随机获取多个标签
func (c *UserTagsCache) RandomTags(ctx context.Context, userID string, count int64) ([]string, error) {
    key := "user:tags:" + userID
    return c.RDB.SRandMemberN(ctx, key, count).Result()
}

移动成员

将成员从一个集合移动到另一个:

// 将标签从一个用户移动到另一个用户
func (c *UserTagsCache) MoveTag(ctx context.Context, fromUserID, toUserID, tag string) error {
    fromKey := "user:tags:" + fromUserID
    toKey := "user:tags:" + toUserID
    return c.RDB.SMove(ctx, fromKey, toKey, tag).Err()
}

性能优化

使用 Pipeline

批量操作多个集合:

func (c *UserTagsCache) AddTagsToMultipleUsers(ctx context.Context, userTags map[string][]string) error {
    pipe := c.RDB.Pipeline()

    for userID, tags := range userTags {
        key := "user:tags:" + userID
        pipe.SAdd(ctx, key, tags)
    }

    _, err := pipe.Exec(ctx)
    return err
}

成员数量限制

避免单个集合成员过多:

func (c *UserTagsCache) Add(ctx context.Context, userID string, tags ...string) error {
    key := "user:tags:" + userID

    // 检查成员数量
    count, err := c.RDB.SCard(ctx, key).Result()
    if err != nil {
        return err
    }

    // 限制最多 100 个标签
    if count+int64(len(tags)) > 100 {
        return errors.New("标签数量超过限制")
    }

    return c.RDB.SAdd(ctx, key, tags).Err()
}

注意事项

  1. 无序性:不要依赖成员的顺序
  2. 唯一性:重复添加相同成员不会报错,但只存储一次
  3. 内存占用:大量成员会占用较多内存
  4. 序列化:复杂类型需要序列化为字符串
  5. 过期时间:整个集合过期,无法为单个成员设置 TTL

On this page