package cache import ( "cmp" "context" "encoding/json" "errors" "math/rand" "sync" "time" ) var ErrKeyNotFound = errors.New("key not found") // Cache 缓存接口 type Cache interface { // Set 设置缓存值,支持TTL Set(key string, value interface{}, ttl time.Duration) // Get 获取缓存值 Get(key string) (interface{}, bool) // Delete 删除缓存 Delete(key string) // DeleteByPrefix 根据前缀删除缓存 DeleteByPrefix(prefix string) // Clear 清空所有缓存 Clear() // Exists 检查键是否存在 Exists(key string) bool // Increment 增加计数器的值 Increment(key string) int64 // IncrementBy 增加指定值 IncrementBy(key string, value int64) int64 // ==================== Hash 操作 ==================== // HSet 设置 Hash 字段 HSet(ctx context.Context, key string, field string, value interface{}) error // HMSet 批量设置 Hash 字段 HMSet(ctx context.Context, key string, values map[string]interface{}) error // HGet 获取 Hash 字段值 HGet(ctx context.Context, key string, field string) (string, error) // HMGet 批量获取 Hash 字段值 HMGet(ctx context.Context, key string, fields ...string) ([]interface{}, error) // HGetAll 获取 Hash 所有字段 HGetAll(ctx context.Context, key string) (map[string]string, error) // HDel 删除 Hash 字段 HDel(ctx context.Context, key string, fields ...string) error // ==================== Sorted Set 操作 ==================== // ZAdd 添加 Sorted Set 成员 ZAdd(ctx context.Context, key string, score float64, member string) error // ZRangeByScore 按分数范围获取成员(升序) ZRangeByScore(ctx context.Context, key string, min, max string, offset, count int64) ([]string, error) // ZRevRangeByScore 按分数范围获取成员(降序) ZRevRangeByScore(ctx context.Context, key string, max, min string, offset, count int64) ([]string, error) // ZRem 删除 Sorted Set 成员 ZRem(ctx context.Context, key string, members ...interface{}) error // ZCard 获取 Sorted Set 成员数量 ZCard(ctx context.Context, key string) (int64, error) // ==================== 计数器操作 ==================== // Incr 原子递增(返回新值) Incr(ctx context.Context, key string) (int64, error) // Expire 设置过期时间 Expire(ctx context.Context, key string, ttl time.Duration) error } const nullMarkerValue = "__carrot_cache_null__" var loadLocks sync.Map type Settings struct { Enabled bool KeyPrefix string DefaultTTL time.Duration NullTTL time.Duration JitterRatio float64 PostListTTL time.Duration ConversationTTL time.Duration UnreadCountTTL time.Duration GroupMembersTTL time.Duration DisableFlushDB bool } var settings = Settings{ Enabled: true, DefaultTTL: 30 * time.Second, NullTTL: 5 * time.Second, JitterRatio: 0.1, PostListTTL: 30 * time.Second, ConversationTTL: 60 * time.Second, UnreadCountTTL: 30 * time.Second, GroupMembersTTL: 120 * time.Second, DisableFlushDB: true, } func Configure(s Settings) { settings.Enabled = s.Enabled if s.KeyPrefix != "" { settings.KeyPrefix = s.KeyPrefix } if s.DefaultTTL > 0 { settings.DefaultTTL = s.DefaultTTL } if s.NullTTL > 0 { settings.NullTTL = s.NullTTL } if s.JitterRatio > 0 { settings.JitterRatio = s.JitterRatio } if s.PostListTTL > 0 { settings.PostListTTL = s.PostListTTL } if s.ConversationTTL > 0 { settings.ConversationTTL = s.ConversationTTL } if s.UnreadCountTTL > 0 { settings.UnreadCountTTL = s.UnreadCountTTL } if s.GroupMembersTTL > 0 { settings.GroupMembersTTL = s.GroupMembersTTL } settings.DisableFlushDB = s.DisableFlushDB } func GetSettings() Settings { return settings } func normalizeKey(key string) string { return cmp.Or(settings.KeyPrefix, "") + ":" + key } func normalizePrefix(prefix string) string { return cmp.Or(settings.KeyPrefix, "") + ":" + prefix } func SetWithJitter(c Cache, key string, value interface{}, ttl time.Duration, jitterRatio float64) { if !settings.Enabled { return } c.Set(key, value, ApplyTTLJitter(ttl, jitterRatio)) } func SetNull(c Cache, key string, ttl time.Duration) { if !settings.Enabled { return } c.Set(key, nullMarkerValue, ttl) } func ApplyTTLJitter(ttl time.Duration, jitterRatio float64) time.Duration { if ttl <= 0 || jitterRatio <= 0 { return ttl } if jitterRatio > 1 { jitterRatio = 1 } maxJitter := int64(float64(ttl) * jitterRatio) if maxJitter <= 0 { return ttl } delta := rand.Int63n(maxJitter + 1) return ttl + time.Duration(delta) } func GetTyped[T any](c Cache, key string) (T, bool) { var zero T if !settings.Enabled { return zero, false } raw, ok := c.Get(key) if !ok { recordMiss() return zero, false } if str, ok := raw.(string); ok && str == nullMarkerValue { recordHit() return zero, false } if typed, ok := raw.(T); ok { recordHit() return typed, true } var out T switch v := raw.(type) { case string: if err := json.Unmarshal([]byte(v), &out); err != nil { recordDecodeError() return zero, false } recordHit() return out, true case []byte: if err := json.Unmarshal(v, &out); err != nil { recordDecodeError() return zero, false } recordHit() return out, true default: data, err := json.Marshal(v) if err != nil { recordDecodeError() return zero, false } if err := json.Unmarshal(data, &out); err != nil { recordDecodeError() return zero, false } recordHit() return out, true } } func GetOrLoadTyped[T any]( c Cache, key string, ttl time.Duration, jitterRatio float64, nullTTL time.Duration, loader func() (T, error), ) (T, error) { if cached, ok := GetTyped[T](c, key); ok { return cached, nil } lockValue, _ := loadLocks.LoadOrStore(key, &sync.Mutex{}) lock := lockValue.(*sync.Mutex) lock.Lock() defer lock.Unlock() if cached, ok := GetTyped[T](c, key); ok { return cached, nil } loaded, err := loader() if err != nil { var zero T return zero, err } encoded, marshalErr := json.Marshal(loaded) if marshalErr == nil && string(encoded) == "null" && nullTTL > 0 { SetNull(c, key, nullTTL) return loaded, nil } SetWithJitter(c, key, loaded, ttl, jitterRatio) return loaded, nil }