竹简文档
缓存

SetCache

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

SetCache

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

接口定义

type SetCache[K any, V any] interface {
    // v1.1.0 起:入参由变参 members ...V 调整为切片 []V,以便在尾部保留 opts ...SetOption 变参位置
    Add(ctx context.Context, key K, members []V, opts ...SetOption) 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

将一组成员添加到集合中。v1.1.0 起入参由变参 members ...V 调整为切片 members []V,并在尾部新增 opts ...SetOption,以便在单次调用中覆盖默认 TTL 或附加写入条件。

Add(ctx context.Context, key K, members []V, opts ...SetOption) error

参数:

  • ctx - context.Context 上下文
  • key - 集合键
  • members - 要添加的成员切片
  • opts - 写操作选项(可选)。常用:
    • xCache.WithTTL(ttl) — 覆盖本次写入的过期时间
    • xCache.WithNX() / WithXX() — 仅当 key 不存在 / 已存在时写入
    • xCache.WithNoSlide() — 添加成员但不延长 key 的整体 TTL

返回值:

  • error - 错误信息

破坏性变更:升级时需将 sc.Add(ctx, key, "a", "b") 改写为 sc.Add(ctx, key, []string{"a", "b"})

示例:

// 沿用默认 TTL
_ = sc.Add(ctx, "tags:1", []string{"golang", "redis"})

// 添加成员但不滑动 TTL
_ = sc.Add(ctx, "tags:1", []string{"docker"}, xCache.WithNoSlide())

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

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 - 错误信息

实现示例

用户标签缓存

通过上下文获取 Manager,再用 SetCacheOf[K, V] 构造泛型集合缓存,无需自行维护 *xCache.CacheRDB 直连。

cache/user_tags.go
import (
    "context"

    xCache "github.com/bamboo-services/bamboo-base-go/major/cache"
    xCtx "github.com/bamboo-services/bamboo-base-go/major/context"
)

// UserTagsCache 用户标签缓存接口,复用泛型 SetCache
type UserTagsCache interface {
    xCache.SetCache[string, string]
}

// NewUserTagsCache 从请求上下文获取 Manager,构造字符串集合缓存
func NewUserTagsCache(ctx context.Context) UserTagsCache {
    manager := ctx.Value(xCtx.CacheManagerKey).(*xCache.Manager)
    sc := xCache.SetCacheOf[string, string](manager)
    return sc
}

基本用法

SetCacheOf 返回的 sc 已实现 Add / Members / IsMember / Count / Remove / Delete 全部方法,可直接调用:

cache/user_tags_usage.go
sc := NewUserTagsCache(ctx)
key := "user:tags:1"

// Add:添加成员(v1.1.0 起使用切片)
_ = sc.Add(ctx, key, []string{"golang", "redis"})

// Add:带 opts,添加成员但不延长 key 的整体 TTL
_ = sc.Add(ctx, key, []string{"docker"}, xCache.WithNoSlide())

// Members:获取所有成员
members, _ := sc.Members(ctx, key)

// IsMember:检查成员是否存在
exists, _ := sc.IsMember(ctx, key, "golang")

// Count:获取成员数量
count, _ := sc.Count(ctx, key)

// Remove:移除指定成员(仍为变参)
_ = sc.Remove(ctx, key, "redis")

// Delete:删除整个集合
_ = sc.Delete(ctx, key)

服务层示例

在服务层封装缓存与数据库的读写协同,Add 统一使用切片传参。

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

func NewUserTagsService(cache UserTagsCache) *UserTagsService {
    return &UserTagsService{cache: cache}
}

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

    // 更新缓存:Add 入参为切片
    key := "user:tags:" + userID
    return s.cache.Add(ctx, key, tags)
}

// GetTags 获取用户标签
func (s *UserTagsService) GetTags(ctx context.Context, userID string) ([]string, error) {
    key := "user:tags:" + userID

    // 先从缓存获取
    tags, err := s.cache.Members(ctx, key)
    if err != nil {
        return nil, err
    }

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

        // 写入缓存:Add 入参为切片
        if len(tags) > 0 {
            _ = s.cache.Add(ctx, key, tags)
        }
    }

    return tags, nil
}

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

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

    // 更新缓存:Remove 仍为变参,用 tags... 展开
    key := "user:tags:" + userID
    return s.cache.Remove(ctx, key, 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. 批量添加

使用切片一次添加多个成员(v1.1.0 起 Add 入参为 []V):

// ✅ 批量添加(切片写法)
sc.Add(ctx, "tags:1", []string{"golang", "redis", "docker"})

// ❌ 逐个添加
sc.Add(ctx, "tags:1", []string{"golang"})
sc.Add(ctx, "tags:1", []string{"redis"})
sc.Add(ctx, "tags:1", []string{"docker"})

2. 成员检查

使用 IsMember 而不是 Members

// ✅ 高效检查
exists, _ := sc.IsMember(ctx, "tags:1", "golang")

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

3. 数量统计

使用 Count 而不是 Members

// ✅ 高效统计
count, _ := sc.Count(ctx, "tags:1")

// ❌ 低效统计
members, _ := sc.Members(ctx, "tags:1")
count := len(members)

4. 空集合处理

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

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

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

    return tags, nil
}

高级操作

集合运算(交集 / 并集 / 差集)、随机获取、成员移动等能力未在 SetCache 接口中暴露,可通过 manager.Redis() 取回底层 Redis 客户端直接调用。

底层扩展用法:直接使用 Redis 客户端进行集合运算,绕过 SetCache 抽象。仅在接口未覆盖的场景下使用,并注意自行管理 key 的命名与 TTL。

集合运算

cache/user_tags_advanced.go
import (
    "context"

    xCache "github.com/bamboo-services/bamboo-base-go/major/cache"
    xCtx "github.com/bamboo-services/bamboo-base-go/major/context"
)

// 底层扩展用法:直接使用 Redis 客户端进行集合运算
func commonTags(ctx context.Context, userID1, userID2 string) ([]string, error) {
    manager := ctx.Value(xCtx.CacheManagerKey).(*xCache.Manager)
    rdb := manager.Redis()

    key1 := "user:tags:" + userID1
    key2 := "user:tags:" + userID2

    // 交集:两个用户的共同标签
    return rdb.SInter(ctx, key1, key2).Result()
}

// 并集:两个用户的所有标签
func allTags(ctx context.Context, userID1, userID2 string) ([]string, error) {
    manager := ctx.Value(xCtx.CacheManagerKey).(*xCache.Manager)
    rdb := manager.Redis()

    key1 := "user:tags:" + userID1
    key2 := "user:tags:" + userID2
    return rdb.SUnion(ctx, key1, key2).Result()
}

// 差集:用户1独有的标签
func uniqueTags(ctx context.Context, userID1, userID2 string) ([]string, error) {
    manager := ctx.Value(xCtx.CacheManagerKey).(*xCache.Manager)
    rdb := manager.Redis()

    key1 := "user:tags:" + userID1
    key2 := "user:tags:" + userID2
    return rdb.SDiff(ctx, key1, key2).Result()
}

随机获取

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

// 底层扩展用法:直接使用 Redis 客户端
manager := ctx.Value(xCtx.CacheManagerKey).(*xCache.Manager)
rdb := manager.Redis()
key := "user:tags:1"

// 随机获取一个标签
tag, _ := rdb.SRandMember(ctx, key).Result()

// 随机获取多个标签
tags, _ := rdb.SRandMemberN(ctx, key, 3).Result()

移动成员

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

// 底层扩展用法:直接使用 Redis 客户端
manager := ctx.Value(xCtx.CacheManagerKey).(*xCache.Manager)
rdb := manager.Redis()

// 将标签从一个用户移动到另一个用户
_ = rdb.SMove(ctx, "user:tags:1", "user:tags:2", "golang").Err()

性能优化

使用 Pipeline

批量操作多个集合(底层扩展用法):

// 底层扩展用法:直接使用 Redis Pipeline 批量写入
manager := ctx.Value(xCtx.CacheManagerKey).(*xCache.Manager)
rdb := manager.Redis()
pipe := rdb.Pipeline()

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

_, err := pipe.Exec(ctx)

成员数量限制

避免单个集合成员过多:

// AddTagsWithLimit 在写入前检查集合大小
func (s *UserTagsService) AddTagsWithLimit(ctx context.Context, userID string, tags []string) error {
    key := "user:tags:" + userID

    // 底层扩展用法:先用 Redis 客户端读取当前数量
    manager := ctx.Value(xCtx.CacheManagerKey).(*xCache.Manager)
    rdb := manager.Redis()
    count, err := rdb.SCard(ctx, key).Result()
    if err != nil {
        return err
    }

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

    // 数量合规后走泛型接口写入
    return s.cache.Add(ctx, key, tags)
}

注意事项

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

On this page