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.Cache 与 RDB 直连。
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 全部方法,可直接调用:
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 统一使用切片传参。
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。
集合运算
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)
}注意事项
- 无序性:不要依赖成员的顺序
- 唯一性:重复添加相同成员不会报错,但只存储一次
- 内存占用:大量成员会占用较多内存
- 序列化:复杂类型需要序列化为字符串
- 过期时间:整个集合过期,无法为单个成员设置 TTL