package manager import ( "context" "encoding/json" "errors" "fmt" "slices" "sync" "time" "github.com/bytedance/sonic" "github.com/redis/go-redis/v9" ) // 缓存适配层 // ICache 定义缓存的标准行为接口 // Get 方法的 value 参数必须为指针类型,用于反序列化结果。 // Set/SetLocal/SetRedis 方法 ttl <= 0 时,默认过期时间为 24 小时。 type ICache interface { Set(ctx context.Context, key string, value any, ttl time.Duration, ids []int64) error Get(ctx context.Context, key string, value any) error // value 必须为指针类型 Del(ctx context.Context, key string) error Remove(ctx context.Context, ids []int64) error BatchDel(ctx context.Context, keys []string) error } var ( ErrCacheNil = errors.New("cache value is nil") ) // CacheRedisHash 以 Redis Hash 结构实现的缓存方案 // 实现原理: // - 所有缓存数据存储在同一个 Redis Hash(keyName)下,不同业务 key 作为 field。 // - 只支持整个 hash 的过期时间(通过 Expire 设置),不支持 field 级别的过期。 // - 适合 field 数量有限、生命周期一致的场景。 // - 若 field 很多且生命周期不一致,建议使用 CacheRedis。 // - 频繁访问某个 field 会导致整个 hash 过期时间被刷新,其他 field 也不会过期,需注意内存风险。 // // 适用场景: // - 业务 key 数量有限,生命周期一致,且需要批量操作 hash 的场景。 type CacheRedisHash struct { options *managerOptions redisClient redis.UniversalClient keyName string hashTableName string // 记录Key=>ids 映射关系的Hash表,field为key,value为ids的json字符串 ttl time.Duration } // NewCacheRedisHash 创建新的 Redis Hash 缓存实例 // ttl <= 0 时,默认过期时间为 24 小时 // keyName 是 Redis Hash 的 key,实际缓存项存储在该 Hash 中,field 为具体的缓存 key // 适用于需要在 Redis 中存储大量相关缓存项的场景,避免过多的 Redis key 导致性能问题 // 这种 hash 结构适合 field 数量有限、生命周期一致的场景。如果 field 很多且生命周期不一致,建议直接用 string 类型的 key-value。 // 例如,可以存储配置项等,一直不需要过期的数据。 func NewCacheRedisHash(redis redis.UniversalClient, keyName string, ttl time.Duration, ops ...OptionFunc) *CacheRedisHash { options := defaultManagerOptions() for _, op := range ops { op(options) } if ttl <= 0 { ttl = 24 * time.Hour } return &CacheRedisHash{ options: options, redisClient: redis, keyName: keyName, hashTableName: keyName + ":hashTable", ttl: ttl, } } func (l *CacheRedisHash) Get(ctx context.Context, key string, value any) error { val, err := l.redisClient.HGet(ctx, l.keyName, key).Result() if err != nil { if err == redis.Nil { return ErrCacheNil // 明确区分缓存未命中和其他错误 } l.options.logger.Errorf(ctx, "cache redis get error: %v", err) return err } err = sonic.Unmarshal([]byte(val), value) if err != nil { l.options.logger.Errorf(ctx, "cache redis unmarshal error: %v", err) return err } l.redisClient.Expire(ctx, l.keyName, l.ttl) return nil } func (r *CacheRedisHash) Set(ctx context.Context, key string, value any, ttl time.Duration, ids []int64) error { jsonBy, err := sonic.Marshal(value) if err != nil { r.options.logger.Errorf(ctx, "cache redis marshal error: %v", err) return err } if ttl <= 0 { ttl = r.ttl // 默认过期时间 } idsBy, err := sonic.Marshal(ids) if err != nil { r.options.logger.Errorf(ctx, "CacheRedisHash Set redis marshal ids key:%s error: %v", key, err) return err } p := r.redisClient.Pipeline() p.HSet(ctx, r.keyName, key, jsonBy) p.Expire(ctx, r.keyName, ttl) // 设置整个 hash 的过期时间,注意这会影响到 hash 中的所有 field p.HSet(ctx, r.hashTableName, key, idsBy) p.Expire(ctx, r.hashTableName, ttl) cmders, err := p.Exec(ctx) if err != nil { r.options.logger.Errorf(ctx, "CacheRedisHash Set redis pipeline exec key:%s error: %v", key, err) return err } // 检查 pipeline 中每个命令的错误 for i, cmd := range cmders { if err := cmd.Err(); err != nil { r.options.logger.Errorf(ctx, "CacheRedisHash Set redis pipeline cmd[%d] error: %v", i, err) return err } } return nil } func (r *CacheRedisHash) Del(ctx context.Context, key string) error { p := r.redisClient.Pipeline() p.HDel(ctx, r.keyName, key) p.HDel(ctx, r.hashTableName, key) cmders, err := p.Exec(ctx) if err != nil { r.options.logger.Errorf(ctx, "CacheRedisHash Del redis pipeline exec key:%s error: %v", key, err) return err } // 检查 pipeline 中每个命令的错误 for i, cmd := range cmders { if err := cmd.Err(); err != nil { r.options.logger.Errorf(ctx, "CacheRedisHash Del redis pipeline cmd[%d] error: %v", i, err) return err } } return nil } func (r *CacheRedisHash) Remove(ctx context.Context, ids []int64) error { // 获取所有 field=>ids 映射关系 hashTable, err := r.redisClient.HGetAll(ctx, r.hashTableName).Result() if err != nil { r.options.logger.Errorf(ctx, "CacheRedisHash Remove redis get keyName:%s error: %v", r.hashTableName, err) return err } for k, v := range hashTable { var inIds []int64 err = sonic.Unmarshal([]byte(v), &inIds) if err != nil { continue } for _, id := range ids { if slices.Contains(inIds, id) { if err := r.Del(ctx, k); err != nil { r.options.logger.Errorf(ctx, "CacheRedisHash Remove Del key:%s error: %v", k, err) } break } } } return nil } func (r *CacheRedisHash) BatchDel(ctx context.Context, keys []string) error { if len(keys) == 0 { return nil } err := r.redisClient.HDel(ctx, r.keyName, keys...).Err() if err != nil { r.options.logger.Errorf(ctx, "cache redis batch del error: %v", err) return err } return nil } func BatchGetRedisHash[T any](r *CacheRedisHash, ctx context.Context, keys []string) (map[string]*T, error) { if len(keys) == 0 { // 批量获取的key为空时,返回空map return map[string]*T{}, nil } res, err := r.redisClient.HMGet(ctx, r.keyName, keys...).Result() if err != nil && !errors.Is(err, redis.Nil) { r.options.logger.Errorf(ctx, "cache redis get error: %v", err) return nil, err } r.options.logger.Infof(ctx, "cache redis get keyName:%s keys: %v len:%d", r.keyName, keys, len(res)) result := make(map[string]*T) for i, v := range res { if v == nil { continue } var t T err := sonic.Unmarshal([]byte(v.(string)), &t) if err != nil { r.options.logger.Errorf(ctx, "cache redis unmarshal error: %v", err) return nil, err } result[keys[i]] = &t } return result, nil } // CacheRedis 以 Redis String 结构实现的缓存方案 // 实现原理: // - 每个缓存 key 独立存储为一个 Redis String,支持单独设置过期时间。 // - 通过 prefix 区分不同业务,避免 key 冲突。 // - 支持泛型批量获取(BatchGetRedis),底层用 pipeline 提高性能。 // - 适合 key 数量较多、生命周期不一致的场景。 // // 适用场景: // - 业务 key 数量较多,生命周期不一致,或需要单独控制每个 key 过期时间。 type CacheRedis struct { options *managerOptions redisClient redis.UniversalClient prefix string // key前缀,避免不同业务之间的key冲突 hashTableName string // 记录Key=>ids 映射关系的Hash表,field为key,value为ids的json字符串 sortSetName string // 记录key的过期时间的有序集合,number为过期的毫秒时间戳,value为key } // NewCacheRedis 创建新的redis缓存实例 func NewCacheRedis(ctx context.Context, redis redis.UniversalClient, prefix string, ops ...OptionFunc) *CacheRedis { options := defaultManagerOptions() for _, op := range ops { op(options) } r := &CacheRedis{ redisClient: redis, prefix: prefix, hashTableName: fmt.Sprintf("%s:hashTable", prefix), sortSetName: fmt.Sprintf("%s:sortSet", prefix), options: options, } go func() { ticker := time.NewTicker(time.Hour) // 定时检查过期key defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: r.clean(ctx) } } }() return r } func (r *CacheRedis) clean(ctx context.Context) error { // 从sortSet查询已过期的key,并删除 now := time.Now().UnixMilli() expiredKeys, err := r.redisClient.ZRangeByScore(ctx, r.sortSetName, &redis.ZRangeBy{ Min: "0", Max: fmt.Sprintf("%d", now), }).Result() if err != nil { r.options.logger.Errorf(ctx, "CacheRedis clean redis ZRangeByScore keyName:%s error: %v", r.sortSetName, err) return err } for _, key := range expiredKeys { r.Del(ctx, key) } return nil } func (r *CacheRedis) Set(ctx context.Context, key string, value any, ttl time.Duration, ids []int64) error { key = fmt.Sprintf("%s:%s", r.prefix, key) // global.Logger.Infof(ctx, "cache redis set key: %s", key) jsonBy, err := sonic.Marshal(value) if err != nil { r.options.logger.Errorf(ctx, "cache redis marshal error: %v", err) return err } if ttl <= 0 { ttl = time.Hour * 24 // 默认过期时间 } idsBy, err := sonic.Marshal(ids) if err != nil { r.options.logger.Errorf(ctx, "CacheRedis Set redis marshal ids key:%s error: %v", key, err) return err } p := r.redisClient.Pipeline() p.Set(ctx, key, jsonBy, ttl) p.ZAdd(ctx, r.sortSetName, redis.Z{ Score: float64(time.Now().Add(ttl).UnixMilli()), Member: key, }) p.HSet(ctx, r.hashTableName, key, idsBy) p.Expire(ctx, r.hashTableName, time.Hour*24*30) // 一个月 cmders, err := p.Exec(ctx) if err != nil { r.options.logger.Errorf(ctx, "CacheRedis Set redis pipeline exec key:%s error: %v", key, err) return err } // 检查 pipeline 中每个命令的错误 for i, cmd := range cmders { if err := cmd.Err(); err != nil { r.options.logger.Errorf(ctx, "CacheRedis Set redis pipeline cmd[%d] error: %v", i, err) return err } } return nil } func (r *CacheRedis) Get(ctx context.Context, key string, value any) error { key = fmt.Sprintf("%s:%s", r.prefix, key) val, err := r.redisClient.Get(ctx, key).Result() if err != nil { if err == redis.Nil { return ErrCacheNil // 明确区分缓存未命中和其他错误 } r.options.logger.Errorf(ctx, "cache redis get error: %v", err) return err } err = sonic.Unmarshal([]byte(val), value) if err != nil { r.options.logger.Errorf(ctx, "cache redis unmarshal error: %v", err) return err } return nil } func (l *CacheRedis) Remove(ctx context.Context, ids []int64) error { // 获取所有 field=>ids 映射关系 hashTable, err := l.redisClient.HGetAll(ctx, l.hashTableName).Result() if err != nil { l.options.logger.Errorf(ctx, "CacheRedis Remove redis get keyName:%s error: %v", l.hashTableName, err) return err } for k, v := range hashTable { var inIds []int64 err = json.Unmarshal([]byte(v), &inIds) if err != nil { continue } for _, id := range ids { if slices.Contains(inIds, id) { if err := l.Del(ctx, k); err != nil { l.options.logger.Errorf(ctx, "CacheRedis Remove Del key:%s error: %v", k, err) } break } } } return nil } // BatchGetRedis 批量获取Redis缓存,支持泛型 func BatchGetRedis[T any](r *CacheRedis, ctx context.Context, keys []string) (map[string]*T, error) { result := make(map[string]*T) if len(keys) == 0 { return result, nil } keyMap := make(map[string]string, len(keys)) redisKeys := make([]string, 0, len(keys)) cmder, err := r.redisClient.Pipeline().Pipelined(ctx, func(pipe redis.Pipeliner) error { for _, key := range keys { rediskey := fmt.Sprintf("%s:%s", r.prefix, key) redisKeys = append(redisKeys, rediskey) keyMap[rediskey] = key // global.Logger.Infof(ctx, "cache redis get key: %s", key) pipe.Get(ctx, rediskey) } return nil }) if err != nil && !errors.Is(err, redis.Nil) { r.options.logger.Errorf(ctx, "BatchGetRedis pipeline exec error: %v", err) return nil, err } for i, cmd := range cmder { stringCom, ok := cmd.(*redis.StringCmd) // r.options.logger.Infof(ctx, "cache redis get key: %s ok:%+v val:%+v", redisKeys[i], ok, stringCom) if !ok { continue } if stringCom.Err() != nil { continue } val := stringCom.Val() key := keyMap[redisKeys[i]] var t T err := sonic.Unmarshal([]byte(val), &t) if err != nil { // 反序列化失败时跳过该项,整体不报错 continue } result[key] = &t } return result, nil } // CacheRedis 删除缓存 保证Key删除的幂等性和一致性 func (r *CacheRedis) Del(ctx context.Context, key string) error { key = fmt.Sprintf("%s:%s", r.prefix, key) p := r.redisClient.Pipeline() // 删除key p.Del(ctx, key) // 从过期时间的有序集合中删除 p.ZRem(ctx, r.sortSetName, key) // 从hashTable中删除 p.HDel(ctx, r.hashTableName, key) cmders, err := p.Exec(ctx) if err != nil { r.options.logger.Errorf(ctx, "CacheRedis Del redis pipeline exec key:%s error: %v", key, err) return err } // 检查 pipeline 中每个命令的错误 for i, cmd := range cmders { if err := cmd.Err(); err != nil { r.options.logger.Errorf(ctx, "CacheRedis Del redis pipeline cmd[%d] error: %v", i, err) return err } } r.options.logger.Infof(ctx, "CacheRedis Del redis del key:%s", key) return nil } func (r *CacheRedis) BatchDel(ctx context.Context, keys []string) error { if len(keys) == 0 { return nil } redisKeys := make([]string, 0, len(keys)) for _, key := range keys { redisKeys = append(redisKeys, fmt.Sprintf("%s:%s", r.prefix, key)) } //global.Logger.Infof(ctx, "CacheRedis BatchDel redis del keys:%+v", redisKeys) err := r.redisClient.Del(ctx, redisKeys...).Err() if err != nil { r.options.logger.Errorf(ctx, "cache redis batch del error: %v", err) return err } return nil } // CacheLocal 本地内存缓存实现 // 实现原理: // - 使用 map[string]item 存储所有缓存,item 结构体包含值和过期时间。 // - 通过互斥锁保证并发安全。 // - 启动后台协程定期清理过期 key。 // - 适合单机场景,缓存容量受限于本地内存。 // // 适用场景: // - 适用于高性能、低延迟、单机进程内缓存需求,如热点数据、本地会话等。 // - 不适合分布式场景或大容量缓存。 type CacheLocal struct { options *managerOptions cacheMap sync.Map keyTableMap sync.Map // 记录Key=>ids 映射关系的Map,field为key,value为ids的数组 quit chan struct{} // 用于停止后台清理协程 } // item 内部存储单元,包含值和过期时间 type item struct { value any expiry int64 // 过期时间戳 (Unix Nano) } func NewCacheLocal(cleanupInterval time.Duration, ops ...OptionFunc) *CacheLocal { options := defaultManagerOptions() for _, op := range ops { op(options) } cache := &CacheLocal{ options: options, cacheMap: sync.Map{}, keyTableMap: sync.Map{}, quit: make(chan struct{}), } // 启动后台清理协程 go func() { ticker := time.NewTicker(cleanupInterval) defer ticker.Stop() for { select { case <-ticker.C: cache.cleanup(context.Background()) case <-cache.quit: return } } }() return cache } // cleanup 定期清理过期的 Key func (c *CacheLocal) cleanup(ctx context.Context) { now := time.Now().UnixNano() c.cacheMap.Range(func(key, value any) bool { it, ok := value.(item) if !ok { return true } if it.expiry > 0 && now > it.expiry { c.cacheMap.Delete(key) // 删除过时的缓存项 } return true }) c.options.logger.Infof(ctx, "CacheLocal cleanup") return } // Stop 停止后台清理协程(通常在程序退出时调用) func (c *CacheLocal) Stop() { select { case <-c.quit: // 已关闭 return default: close(c.quit) } } // Set 将 value 存储为 interface{},建议 value 为可安全复制的类型 func (l *CacheLocal) Set(ctx context.Context, key string, value any, ttl time.Duration, ids []int64) error { if ttl <= 0 { ttl = time.Hour * 24 // 默认过期时间 } // 存储深拷贝,防止外部修改 var v any switch vv := value.(type) { case nil: v = nil case string, int, int64, float64, bool: v = vv default: // 对于结构体、切片、map等,序列化再反序列化实现深拷贝 by, err := sonic.Marshal(vv) if err != nil { return err } err = sonic.Unmarshal(by, &v) if err != nil { return err } } l.cacheMap.Store(key, item{v, time.Now().Add(ttl).UnixNano()}) l.keyTableMap.Store(key, ids) return nil } // Get 直接类型断言赋值,提升类型安全 func (l *CacheLocal) Get(ctx context.Context, key string, value any) error { itemData, exists := l.cacheMap.Load(key) if !exists { return ErrCacheNil // 不存在,返回nil表示缓存未命中 } item, ok := itemData.(item) if !ok { return ErrCacheNil // 类型断言失败,返回nil表示缓存未命中 } if time.Now().UnixNano() > item.expiry { l.cacheMap.Delete(key) // 删除过期的缓存 return ErrCacheNil } // 直接类型断言赋值 switch v := value.(type) { case *string: vv, ok := item.value.(string) if !ok { return errors.New("cache type mismatch: expect string") } *v = vv case *int: vv, ok := item.value.(int) if !ok { return errors.New("cache type mismatch: expect int") } *v = vv case *int64: vv, ok := item.value.(int64) if !ok { return errors.New("cache type mismatch: expect int64") } *v = vv case *float64: vv, ok := item.value.(float64) if !ok { return errors.New("cache type mismatch: expect float64") } *v = vv case *bool: vv, ok := item.value.(bool) if !ok { return errors.New("cache type mismatch: expect bool") } *v = vv default: // 结构体、切片、map等,序列化再反序列化 by, err := sonic.Marshal(item.value) if err != nil { return err } err = sonic.Unmarshal(by, value) if err != nil { return err } } return nil } // BatchGetLocal 批量获取本地缓存,支持泛型 func BatchGetLocal[T any](c *CacheLocal, ctx context.Context, keys []string) (map[string]*T, error) { result := make(map[string]*T) for _, key := range keys { var t T err := c.Get(ctx, key, &t) if err != nil && !errors.Is(err, ErrCacheNil) { return nil, err } if errors.Is(err, ErrCacheNil) { continue } result[key] = &t } return result, nil } func (l *CacheLocal) BatchDel(ctx context.Context, keys []string) error { return nil } func (l *CacheLocal) Del(ctx context.Context, key string) error { l.cacheMap.Delete(key) // 删除指定的缓存 l.keyTableMap.Delete(key) return nil } func (l *CacheLocal) Remove(ctx context.Context, ids []int64) error { l.keyTableMap.Range(func(key, value any) bool { idsArr, ok := value.([]int64) if !ok { return true } keyStr, ok := key.(string) if !ok { return true } for _, id := range idsArr { for _, id2 := range ids { if id == id2 { l.Del(ctx, keyStr) } } } return true }) return nil }