2026-03-09 21:28:58 +08:00
|
|
|
|
package cache
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2026-03-17 00:47:17 +08:00
|
|
|
|
"cmp"
|
2026-03-09 21:28:58 +08:00
|
|
|
|
"context"
|
|
|
|
|
|
"encoding/json"
|
2026-03-20 12:23:28 +08:00
|
|
|
|
"errors"
|
2026-03-09 21:28:58 +08:00
|
|
|
|
"math/rand"
|
|
|
|
|
|
"sync"
|
|
|
|
|
|
"time"
|
2026-03-24 05:18:30 +08:00
|
|
|
|
|
|
|
|
|
|
"github.com/redis/go-redis/v9"
|
2026-03-09 21:28:58 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2026-03-20 12:23:28 +08:00
|
|
|
|
var ErrKeyNotFound = errors.New("key not found")
|
|
|
|
|
|
|
2026-03-09 21:28:58 +08:00
|
|
|
|
// Cache 缓存接口
|
|
|
|
|
|
type Cache interface {
|
|
|
|
|
|
// Set 设置缓存值,支持TTL
|
2026-03-30 04:49:35 +08:00
|
|
|
|
Set(key string, value any, ttl time.Duration)
|
2026-03-09 21:28:58 +08:00
|
|
|
|
// Get 获取缓存值
|
2026-03-30 04:49:35 +08:00
|
|
|
|
Get(key string) (any, bool)
|
2026-03-09 21:28:58 +08:00
|
|
|
|
// 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
|
2026-05-04 13:07:03 +08:00
|
|
|
|
// DeleteBatch 批量删除多个 key(使用 Pipeline 减少网络往返)
|
|
|
|
|
|
DeleteBatch(keys []string)
|
2026-03-12 08:38:14 +08:00
|
|
|
|
|
|
|
|
|
|
// ==================== Hash 操作 ====================
|
|
|
|
|
|
// HSet 设置 Hash 字段
|
2026-03-30 04:49:35 +08:00
|
|
|
|
HSet(ctx context.Context, key string, field string, value any) error
|
2026-03-12 08:38:14 +08:00
|
|
|
|
// HMSet 批量设置 Hash 字段
|
2026-03-30 04:49:35 +08:00
|
|
|
|
HMSet(ctx context.Context, key string, values map[string]any) error
|
2026-03-12 08:38:14 +08:00
|
|
|
|
// HGet 获取 Hash 字段值
|
|
|
|
|
|
HGet(ctx context.Context, key string, field string) (string, error)
|
|
|
|
|
|
// HMGet 批量获取 Hash 字段值
|
2026-03-30 04:49:35 +08:00
|
|
|
|
HMGet(ctx context.Context, key string, fields ...string) ([]any, error)
|
2026-03-12 08:38:14 +08:00
|
|
|
|
// 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)
|
2026-03-24 05:18:30 +08:00
|
|
|
|
// ZRevRange 按排名倒序取成员(score 从高到低,start/stop 为排名下标,含端点)
|
|
|
|
|
|
ZRevRange(ctx context.Context, key string, start, stop int64) ([]string, error)
|
|
|
|
|
|
// ZReplaceSortedSet 原子替换整个 ZSET(先删后批量写入)
|
|
|
|
|
|
ZReplaceSortedSet(ctx context.Context, key string, members []redis.Z) error
|
2026-03-12 08:38:14 +08:00
|
|
|
|
// ZRem 删除 Sorted Set 成员
|
2026-03-30 04:49:35 +08:00
|
|
|
|
ZRem(ctx context.Context, key string, members ...any) error
|
2026-03-12 08:38:14 +08:00
|
|
|
|
// 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
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const nullMarkerValue = "__carrot_cache_null__"
|
|
|
|
|
|
|
|
|
|
|
|
var loadLocks sync.Map
|
|
|
|
|
|
|
|
|
|
|
|
type Settings struct {
|
2026-03-12 08:38:14 +08:00
|
|
|
|
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
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 {
|
2026-03-17 00:47:17 +08:00
|
|
|
|
return cmp.Or(settings.KeyPrefix, "") + ":" + key
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-24 05:18:30 +08:00
|
|
|
|
// ResolveKey 返回带全局前缀的实际 Redis 键(供需直连 Redis 的组件使用)
|
|
|
|
|
|
func ResolveKey(key string) string {
|
|
|
|
|
|
return normalizeKey(key)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 21:28:58 +08:00
|
|
|
|
func normalizePrefix(prefix string) string {
|
2026-03-17 00:47:17 +08:00
|
|
|
|
return cmp.Or(settings.KeyPrefix, "") + ":" + prefix
|
2026-03-09 21:28:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-30 04:49:35 +08:00
|
|
|
|
func SetWithJitter(c Cache, key string, value any, ttl time.Duration, jitterRatio float64) {
|
2026-03-09 21:28:58 +08:00
|
|
|
|
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 {
|
2026-03-13 09:38:18 +08:00
|
|
|
|
recordMiss()
|
2026-03-09 21:28:58 +08:00
|
|
|
|
return zero, false
|
|
|
|
|
|
}
|
|
|
|
|
|
if str, ok := raw.(string); ok && str == nullMarkerValue {
|
2026-03-13 09:38:18 +08:00
|
|
|
|
recordHit()
|
2026-03-09 21:28:58 +08:00
|
|
|
|
return zero, false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if typed, ok := raw.(T); ok {
|
2026-03-13 09:38:18 +08:00
|
|
|
|
recordHit()
|
2026-03-09 21:28:58 +08:00
|
|
|
|
return typed, true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var out T
|
|
|
|
|
|
switch v := raw.(type) {
|
|
|
|
|
|
case string:
|
|
|
|
|
|
if err := json.Unmarshal([]byte(v), &out); err != nil {
|
2026-03-13 09:38:18 +08:00
|
|
|
|
recordDecodeError()
|
2026-03-09 21:28:58 +08:00
|
|
|
|
return zero, false
|
|
|
|
|
|
}
|
2026-03-13 09:38:18 +08:00
|
|
|
|
recordHit()
|
2026-03-09 21:28:58 +08:00
|
|
|
|
return out, true
|
|
|
|
|
|
case []byte:
|
|
|
|
|
|
if err := json.Unmarshal(v, &out); err != nil {
|
2026-03-13 09:38:18 +08:00
|
|
|
|
recordDecodeError()
|
2026-03-09 21:28:58 +08:00
|
|
|
|
return zero, false
|
|
|
|
|
|
}
|
2026-03-13 09:38:18 +08:00
|
|
|
|
recordHit()
|
2026-03-09 21:28:58 +08:00
|
|
|
|
return out, true
|
|
|
|
|
|
default:
|
|
|
|
|
|
data, err := json.Marshal(v)
|
|
|
|
|
|
if err != nil {
|
2026-03-13 09:38:18 +08:00
|
|
|
|
recordDecodeError()
|
2026-03-09 21:28:58 +08:00
|
|
|
|
return zero, false
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := json.Unmarshal(data, &out); err != nil {
|
2026-03-13 09:38:18 +08:00
|
|
|
|
recordDecodeError()
|
2026-03-09 21:28:58 +08:00
|
|
|
|
return zero, false
|
|
|
|
|
|
}
|
2026-03-13 09:38:18 +08:00
|
|
|
|
recordHit()
|
2026-03-09 21:28:58 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|